COM技术5

来源:互联网 发布:用阿里云搭建网站 编辑:程序博客网 时间:2024/05/16 09:03

 

     在前面四章中,可以看到将客户和组件程序分开是大势所趋,我以前的思考是组件程序能编译成一份obj,然后客户端就直接用这个obj,而不再编译组件部分的程序.这样,就算组件部分程序有变化,只要替换掉obj再编译客户端的程序就可以了.所以我做了尝试,把组件和客户端的代码分开到独立的文件中.这其实还是属于静态链接的形式,因为决定程序用哪个组件提供的接口是在编译期间根据不同的obj来决定的,如果一个组件提供了很多个接口,客户1如果只需要用一个,但它也必需把这很多个接口包含进来,因为这些接口都包含在一个obj文件中,是一个整体.如果客户2也需要用其中一个接口,那它也需要copy这个obj,然后编译,存在多少个客户,就存在多少个obj拷贝.对于静态编译的理解,下面的文章做了很好的说明,它来源于一个很出色的网站,这个网站对如何从零开始手动编译并运行一个linux做了探讨:

摘自:http://docs.huihoo.com/gnu_linux/lfs/lfs-4.0/index.html
我们为什么使用静态链接?
(向Plasmatic致谢,本文建立在他贴在LFS邮件列表的一篇文章基础之上。)

当写程序的时候,我们不用每次都把所有的函数重写,比如处理内核、硬件、文件等等的函数,具有一些共性。所以这些函数就保存在库文件中,供你下次写程序时使用。glibc,你后面将安装的,就是这样一个大的库文件,包含了所有基本函数供程序使用,比如打开文件,在屏幕上打印信息,从使用者得到反馈。当编译程序的时候,这些库的代码被链接到程序中,以便程序使用库提供的函数。

然而,这些库文件有可能非常大(比如libc.a经常有2.5MB大),你可能不希望在每个程序中包含一份拷贝。只要设想一下你的简单命令(比如ls)竟要附加2.5MB的库! 所以,我们不是把库作为程序的真正部份(也就是静态链接),而是把库存成单独的文件,当程序需要的时候再把它装载上来。这就是我们所说的动态链接,因为根据程序的需要,库动态地装载和卸载。

所以,现在我们有一个1KB的文件和一个2.5MB的文件,但是我们还没有节省空间。动态链接的真正优点是每个库我们只需要一份拷贝。如果ls和rm都要使用同样的库,那么我们就不需要两份拷贝,因为它们都能从同一个库文件中得到所要的函数。即使在内存中,这两个程序也共享同样的代码,而不是装载两份一样的代码到内存中。所以我们不仅节约了硬盘空间,还节约了宝贵的内存。

如果动态链接节约了这么多空间,那么在这一章里我们为什么要用静态链接呢?这是因为当你使用chroot命令(在下一章里将说到)进入你崭新(但是还不完整)的LFS系统,这些动态链接的库将不能使用,因为它们放在你的老目录树中(比如/usr/lib),这样的目录在你的LFS目录树中($LFS)是不能存取的。

所以为了你的新程序能在chroot的环境下运行,你就要用静态链接来编译它们,于是--enable-static-link,--disable-shared, 和-static标志在整个第五章中都要使用。一旦到了第六章,我们要做的第一件事就是重新编译glibc。做好后,我们将重新编译所有第五章的程序,但这次将用动态链接,所以最终我们能够节约空间。

这就是你必须使用那些怪异的 -static 标志的原因。如果你试图不用静态链接来编译本章的程序(千万不要这样),当你用chroot进入新系统时,你就会看到不幸的事(程序不能运行)。

如果你想了解更多动态链接库的知识,看看程序设计的书或网站,尤其是Linux相关的。

 

    这章将讨论一种新的思路,也就是对应的动态链接,即如何将组件放入动态链接库(DLL)中.需要说明的是,我们并不是把组件变为一个DLL.这两个概念不是等同的.DLL只是一个组件服务器,或者发行组件的方式.组件实际上应看成是在DLL中所实现的接口集.DLL只是一个形式,而组件才是实质.

    下面会建立一个这样的示例,在程序中会有些特殊的说明,我通过注释形式说明.首先来看看文件构成:

   

    需要关注的应该是共享文件,因为这是组件提供给客户的使用手册.那我们就先来看看这两个文件有些什么东西:

 IFACE.h 

//
//Iface.h
//
//Interfaces

interface IX: IUnknown
{
    
virtual void _stdcall Fx() = 0;
}
;

interface IY: IUnknown
{
    
virtual void _stdcall Fy() = 0;
}
;

interface IZ: IUnknown
{
    
virtual void _stdcall Fz() = 0;
}
;

//Forward references for GUIDs
extern "C"
{
    
extern const IID IID_IX;
    
extern const IID IID_IY;
    
extern const IID IID_IZ;
}

 

GUIDs.cpp

//
//GUIDs.cpp - Interface IDs
//
#include <objbase.h>

extern "C"
{
    
extern const IID IID_IX = 
    
{
        
0x32bb8320,0xb41b,0x11cf,{0xa6,0xbb,0x0,0x80,0xc7,0xb2,0xd6,0x82}
    }
;
    
extern const IID IID_IY = 
    
{
        
0x32bb8321,0xb41b,0x11cf,{0xa6,0xbb,0x0,0x80,0xc7,0xb2,0xd6,0x82}
    }
;
    
extern const IID IID_IZ = 
    
{
        
0x32bb8322,0xb41b,0x11cf,{0xa6,0xbb,0x0,0x80,0xc7,0xb2,0xd6,0x82}
    }
;
    
}

    在这章的代码中,我们可以清楚的看到,组件应该为客户程序提供的是什么,一个是IID,一个是接口定义.这两部分其实是相对稳定的部分,我们再来看组件部分的代码:

CMPNT1.cpp 

//
//cmpnt1.cpp
//to compile,use:cl/LD Cmpnt1.cpp GUIDs.cpp UUID.lib Cmpnt1.def
//

#include 
<iostream.h>
#include 
<objbase.h>

#include 
"Iface.h"

void trace(const char* msg){cout << "Component 1: " << msg << endl;}

//
//Component
//
class CA : public IX
{
    
//IUnknown implementation
    virtual HRESULT _stdcall QueryInterface(const IID& iid,void** ppv);
    
virtual ULONG _stdcall AddRef();
    
virtual ULONG _stdcall Release();

    
//Interface IX implementation
    virtual void _stdcall Fx(){cout << "Fx" << endl;}
public:
    
//Constructor
    CA():m_cRef(0){}
    
    
//Destructor
    ~CA() {trace("Destroy self.");}
private:
    
long m_cRef;
}
;

HRESULT _stdcall CA::QueryInterface(
const IID&iid, void** ppv)
{
    
if (iid == IID_IUnknown)
        
{
        trace(
"Return pointer to IUnknown");
        
*ppv = static_cast<IX*>(this);
    }

    
else if(iid == IID_IX)
    
{
        trace(
"Return pointer to IX.");
        
*ppv = static_cast<IX*>(this);
    }

    
else 
    
{
        trace(
"Return pointer to IX.");
        
*ppv = NULL;
        
return E_NOINTERFACE;
    }

    reinterpret_cast
<IUnknown*>(*ppv)->AddRef();
    
return S_OK;
}


ULONG _stdcall CA::AddRef()
{
    
return InterlockedIncrement(&m_cRef);
}


ULONG _stdcall CA::Release()
{
    
if (InterlockedDecrement(&m_cRef) == 0)
    
{
        delete 
this;
        
return 0;
    }

    
return m_cRef;
}



//Creation function
/*
在函数的定义前加上extern "C"可防止C++编译器在函数名称上加上类型信息.如果不加,VC可能会把CreateInstance变成: ?CreateInstance@@YAPAUUnknown@@XZ 其他编译器还可能有其他的修改方法.这并没有什么标准,所以为了移植性,还是加上.另外,这种变换以后的名称处理起来也非常困难.另外我想,在后面的def中,要写输出函数的名字,如果不用extern "C",是不是要写被vc变换后的名字,那就太夸张了.--后来做了尝试,把extern "C"去掉,程序依然能够正确运行,这也从一方面说明了用extern "C"主要还是从可移植性考虑.
*/

extern "C" IUnknown* CreateInstance()
{
    IUnknown
* pI = static_cast<IX*>(new CA);
    pI
->AddRef();
    
return pI;
}

 

    然后还需要一个.DEF文件,他说明了链接程序需要输出什么函数,.DEF文件如下:

CMPNT1.DEF

;
; Cmpnt1 module
-definition file
;

LIBRARY         Cmpnt1.dll
DESCRIPTION     
'(c)2007-2008 LANCE'

EXPORTS
                CreateInstance @
1    PRIVATE

    这个DEF文件主要是EXPORTS段中列出从DLL中输出的函数的名称.对每一个名称,还可以加上一个序号.在LIBRARY行上必须加上DLL的实际名称.从DLL中输出函数所需要做的工作就是这些.下面可以在命令行中用下面的语句编译了,后面还说明了cl的相关帮助文件: 

cl /LD Cmpnt1.cpp GUIDS.cpp UUID.lib Cmpnt1.def

/*
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8168 for 80x86
Copyright (C) Microsoft Corp 1984-1998. All rights reserved.

                          C/C++ COMPILER OPTIONS

                              -OPTIMIZATION-

/O1 minimize space                       /Op[-] improve floating-pt consistency
/O2 maximize speed                       /Os favor code space
/Oa assume no aliasing                   /Ot favor code speed
/Ob<n> inline expansion (default n=0)    /Ow assume cross-function aliasing
/Od disable optimizations (default)      /Ox maximum opts. (/Ogityb1 /Gs)
/Og enable global optimization           /Oy[-] enable frame pointer omission
/Oi enable intrinsic functions

                             -CODE GENERATION-

/G3 optimize for 80386                   /Gy separate functions for linker
/G4 optimize for 80486                   /Ge force stack checking for all funcs
/G5 optimize for Pentium                 /Gs[num] disable stack checking calls
/G6 optimize for Pentium Pro             /Gh enable hook function call
/GB optimize for blended model (default) /GR[-] enable C++ RTTI
/Gd __cdecl calling convention           /GX[-] enable C++ EH (same as /EHsc)
/Gr __fastcall calling convention        /Gi[-] enable incremental compilation
/Gz __stdcall calling convention         /Gm[-] enable minimal rebuild
/GA optimize for Windows Application     /EHs enable synchronous C++ EH
/GD optimize for Windows DLL             /EHa enable asynchronous C++ EH

/Gf enable string pooling                /EHc extern "C" defaults to nothrow
/GF enable read-only string pooling      /QIfdiv[-] enable Pentium FDIV fix
/GZ enable runtime debug checks          /QI0f[-] enable Pentium 0x0f fix

                              -OUTPUT FILES-

/Fa[file] name assembly listing file     /Fo<file> name object file
/FA[sc] configure assembly listing       /Fp<file> name precompiled header file
/Fd[file] name .PDB file                 /Fr[file] name source browser file
/Fe<file> name executable file           /FR[file] name extended .SBR file
/Fm[file] name map file

                              -PREPROCESSOR-

/C don't strip comments                  /FI<file> name forced include file
/D<name>{=|#}<text> define macro         /U<name> remove predefined macro
/E preprocess to stdout                  /u remove all predefined macros
/EP preprocess to stdout, no #line       /I<dir> add to include search path
/P preprocess to file                    /X ignore "standard places"

                                -LANGUAGE-

/Zi enable debugging information         /Zl omit default library name in .OBJ
/ZI enable Edit and Continue debug info  /Zg generate function prototypes

/Z7 enable old-style debug info          /Zs syntax check only
/Zd line number debugging info only      /vd{0|1} disable/enable vtordisp
/Zp[n] pack structs on n-byte boundary   /vm<x> type of pointers to members
/Za disable extensions (implies /Op)     /noBool disable "bool" keyword
/Ze enable extensions (default)

                              -MISCELLANEOUS-

/?, /help print this help message        /V<string> set version string
/c compile only, no link                 /w disable all warnings
/H<num> max external name length         /W<n> set warning level (default n=1)
/J default char type is unsigned         /WX treat warnings as errors
/nologo suppress copyright message       /Yc[file] create .PCH file
/Tc<source file> compile file as .c      /Yd put debug info in every .OBJ
/Tp<source file> compile file as .cpp    /Yu[file] use .PCH file
/TC compile all files as .c              /YX[file] automatic .PCH
/TP compile all files as .cpp            /Zm<n> max memory alloc (% of default)

                                 -LINKING-

/MD link with MSVCRT.LIB                 /MDd link with MSVCRTD.LIB debug lib
/ML link with LIBC.LIB                   /MLd link with LIBCD.LIB debug lib
/MT link with LIBCMT.LIB                 /MTd link with LIBCMTD.LIB debug lib
/LD Create .DLL                          /F<num> set stack size

/LDd Create .DLL debug libary            /link [linker options and libraries]
*/

    在cmd中可以使用dumpbin -exports Cmpnt1.dll可以查看DLL中输出符号的清单.查出的结果如下图所示:

/*
F:My DocumentsBlog-12-05IFace>dumpbin -exports cmpnt1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file cmpnt1.dll

File Type: DLL

  Section contains the following exports for Cmpnt1.dll

           0 characteristics
    47568CD4 time date stamp Wed Dec 05 19:34:44 2007
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 0000111C CreateInstance

  Summary

        2000 .data
        1000 .rdata
        1000 .reloc
        6000 .text

*/

    如果你用cl命令时,忘了最后的.DEF文件,那么你编译链接后看到的界面应该如下所示:

cl /LD Cmpnt1.cpp GUIDS.cpp UUID.lib

/*
F:My DocumentsBlog-12-05IFace>dumpbin -exports cmpnt1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file cmpnt1.dll

File Type: DLL

  Summary

        2000 .data
        1000 .rdata
        1000 .reloc
        6000 .text
*/

    这也说明.DEF文件是必须的,少了.DEF文件,客户将无法使用此函数.

    下面来看客户端的代码:

Create.h

//
// Create.h
//

IUnknown
* CallCreateInstance(char* name) ;

Create.cpp

//
//Create.cpp
//

#include 
<iostream.h>
#include 
<unknwn.h> //Declare IUnknown.
#include "Create.h"

typedef IUnknown
* (*CREATEFUNCPTR)();
IUnknown
* CallCreateInstance(char* name)
{
    
//Load dynamic link library into process.
    HINSTANCE hComponent = ::LoadLibrary(name);
    
if (hComponent == NULL)
    
{
        cout 
<< "CallCreateInstance: Error:Cannot load component" << endl;
        
return NULL;
    }


    
//Get address for CreateInstance function.
    CREATEFUNCPTR CreateInstance = (CREATEFUNCPTR)::GetProcAddress(hComponent,"CreateInstance");
    
if (CreateInstance == NULL)
    
{
        cout 
<<"Call CreateInstance:  Error:"
             
<<"Cannot find CreateInstance function."
             
<<endl;
        
return NULL;
    }

    
return CreateInstance();
}

Client1.cpp

#include <iostream.h>
#include 
<objbase.h>

#include 
"Iface.h"
#include 
"Create.h"

void trace(const char* msg){cout << "Client 1:  " << msg << endl;}

//
//Client1
//

int main()
{
    HRESULT hr;
    
//Get the name of the component to use.
    char name[40];
    cout 
<< "Enter the filename of a component to use [Cmpnt?.dll]:";
    cin 
>> name;
    cout 
<< endl;

    
//Create component by calling the CreateInstance function in the DLL.
    trace("Get an IUnknown pointer.");
    IUnknown
* pIUnKnown = CallCreateInstance(name);
    
if (pIUnKnown == NULL)
    
{
        trace(
"CallCreateInstance Failed.");
        
return 1;
    }

    
    trace(
"Get interface IX.");
    IX
* pIX = NULL;
    hr 
= pIUnKnown->QueryInterface(IID_IX,(void**)&pIX);

    
if (SUCCEEDED(hr))
    
{
        trace(
"Succeeded getting IX.");
        pIX
->Fx();            //Use iterface IX.
        pIX->Release();
    }

    
else
    
{
        trace(
"Could not get interface IX.");
    }


    trace(
"Release IUnknown interface.");
    pIUnKnown
->Release();

    
return 0;

}

    Create.h和Create.cpp实现了函数CallCreateInstance.这个函数以DLL的名称做为参数.其功能是装载此DLL并调用其中所输出的函数CreateInstance.这个函数可以建立一个组件的实例并给客户返回一个IUnknown接口的指针.这是DLL中唯一需要客户显示链接的函数.组件中客户所需要的所有函数都可以通过这个接口指针而访问到.在CallCreateInstance中调用了Win32的LoadLibrary函数,这函数以被装载的DLL的名称作为参数并返回一个指向所装载的DLL的句柄.Win32的GetProcAddress函数可以使用此句柄以及待调用的函数的名称,返回一个指向此函数的指针.使用这两个Win32函数,客户可以将所需要的DLL装载到其地址空间中并获取CreateInstance的地址.有了此地址之后,创建所需的组件并获取其IUnknown接口的指针就是很简单的事情了.

    但CallCreateInstance仍使客户和其组件的实现之间是一种紧密的联系.因为客户并不需要知道实现组件的DLL的名称.在后面的章节中我们将讨论一种更为一般和灵活的方法,以便客户和组件彻底地分开.在第7章中,CallCreateInstance将被一个名为CoCreateInstance的COM库函数所代替.

  最后来看看为什么可以使用DLL来实现组件,真正的原因在于DLL可以共享它们所链入的应用程序的地址空间.前面的章节可以看出,客户和组件是通过接口进行交互的.一个接口实际上是一个指向函数的指针列表(vtbl).组件将为vtbl分配内存并用每一个函数的地址来初始化此表格.为使用vtbl,客户应该能够访问组件为其vtbl所分配的内存.同时它还必须能够理解组件放入到vtbl中的各个地址.在Windows中,由于动态链接库与客户使用的是同一地址空间,因此客户访问vtbl是不成问题的.
    在windows中一个正在被执行的程序被称作一个进程.每一个应用程序(EXE)都将以一个单独的进程运行,每一个进程都有一个4GB的地址空间.一个进程中的一个地址同另外一个进程中的某个地址是不同的.由于指针是在不同的地址空间中起作用的,因此不能将一个指针从一个进程传到另外一个进程.换句话说,两个不同进程中的指针可以包含相同的地址值,但它们实际上指向的是不同的物理内存.但幸运的是,动态链接库将驻留在所链接的应用程序的地址空间中,由于DLL和EXE共享同一进程,因此它们也可以共享同一地址空间.由于这个原因,Dll经常也被称作是进程中的服务器(in-process server).在第10章我们将讨论以EXE方式实现的"进程外服务器",或"本地及远程服务器".进程外服务器具有与其客户不同的地址空间,但我们将仍然使用DLL来帮助实现进程外服务器同其客户之间的交流.下图显示了DLL是如何映射到其客户的地址空间的.


    此处的关键在于,当客户得到组件的一个接口指针时,连接客户和组件的唯一中介是接口的二进制结构.当客户查询组件的某个接口时,它所请求的实际上是具有特定格式的一块内存.当组件返回一个接口指针时,它告诉客户的实际上是次块内存的地址.由于接口是在客户和组件都能访问的内存中,因此这种情况实际上与当客户和组件在同一EXE文件中是相同的.对于客户而言,唯一的区别在于在静态链接及动态链接的情况下它获取接口指针的方式不同.