在本章中我们要学习的是将组件放入动态链接库(DLL)中。DLL是是一个组件服务器,也可以说是一种发行组件的方式,我们在这里要清楚:一个组件并不是一个DLL,组件实际上应该是在DLL中时间的接口集。DLL只是一种形式,而组件才是实质。
组件的创建
在客户可以获取某个组件接口指针之前,它必须先将相应的DLL装载到其进程空间中并创建此组件。第3章中给出的CreateInstance可以建立一个组件的实例并给客户返回一个IUnknown接口指针。这是DLL中唯一需要客户显式链接的函数。由于组件中客户所需的所有函数都可以通过一个接口指针而访问到,因此需要在DLL中输出 CreateInstance 函数以便客户可以调用它。
从DLL中输出函数
要从DLL中输出一个函数,需要先把函数用extem"C"
进行标记
// Creation function
extern "c"
IUnknown * CreateInstance()
{
IUnknown * pI = (IUnknown * )(void * ) new CA;
pI-> AddRef() :
return pI ;
}
在函数的定义前加上extem"C"
可防止C++编译器在函数名称上加上类型信息,从而确保函数可以被 C 语言或者其他使用 C 链接规范的代码正确调用。如不加 extem“C”,Microsoft Visual C++将把 CreateInstance 变成:
? CreateInstance@ @YAPAUUnknown@ @XZ
其他的编译器可能会使用另外某种名称修改方法。对此并没有什么标准,因此变换以后的函数是不可移植的。并且对这种变换以后的名称处理起来也非常困难。
当然从DLL中输出函数仅仅是将函数加上extern“C”标记是不够的,还需要告诉链接程序需要输出什么函数。为此,需要建立一个DEF函数。
DEF文件建立起来也是比较麻烦的。为此可以在某个例子中复制一个DEF文件,然后修改其中的某些行。在程序清单5-1中给出了CMPNT1,DLL的 DEF文件 CMPNT1.DEF
程序清单5-1 在模块定义文件中列出了动态链接库所输出的函数的名称。
CMPNTI.DEF
// Compnt1 module definition file.Compnt1模块定义文件。
LIBRARY Compnti.dil
DESCRIPTION '(c) 1996-1997 Dale E.Rogerson'
EXPORTS
CreateInstance @1 PRIVATE:
对上述文件所需要做的全部工作就是在EXPORTS段中列出待从DLL中输出的函数的名称。对每一个名称,还可以加上一个序号。在LIBRARY行上必须加上DLL的实际名称。
从DLL中输出函数所需要做的工作就是这些。下面我们看一下如何装载所需的DLL并调用其中的函数。
DLL的装载
在文件 CREATE.H 和 CREATE.CPP中实现了函数 CallCreatelnstance
。这个函数以DLL的名称作为参数。其功能是装载此DLL并调用其中所输出的函数Createlnstance。其代码如程序清单5-2所示。
// Create.cpp
#include<iostream>
#include <unknwn.h>
#include"Create.h"
typedef IUnknown* (*CREATEFUNCPTR)();
IUnknown* CallCreateInstance(char* name)
{
// Load dynamic link library into process.将动态链接库加载到进程中。
HINSTANCE hComponent = ::LoadLibrary(name);
if (hCamponent == NULL)
{
cout << "CallCreateInstance: \ tError: Cannot load component." << endl;
return NULL;
}
// Get address for CreateInstance function.获取CreateInstance函数的地址。
CREATEFUNCPTR CreateInstance
= (CREATEFUNCPTR) ::GetProcAddress(hComponent, "CreateInstance");
if (CreateInstance == NULL)
{
cout << "CallCreateInstance: \ tError:"
<< "Cannot find CreateInstance function."//"找不到CreateInstance函数。"
<< endl;
return NULL;
}
return CreateInstance();
}
为装载指定的DLL,CallCreatInatsnce
调用了Win32的LoadLibrary
函数,以被装载的DLL的名称作为参数,并返回一个指向所装载的DLL的句柄:
HISTANCE LoadLibrary(
LPCTSTR lpLibFileName // filename of DLL DLL的文件名
) ;
Win32的GetProcAddress
函数可以使用LoadLibrary
函数返回的句柄以及待调用的函数的名称,然后返回一个指向此函数的指针:
FARPROC GetProcAddress(
HMODULE hModule, // handle to DLL module DLL模块的句柄
LPCSTR lpProcName // name of function 函数名
)
使用上述两个函数,客户可以将所需的DLL装载到其地址空间中并获取Createlnstance的地址,以创建所需的组件并获取其IUnknown接口的指针。然后CallCreateInstance将把此地址转换成一个可以使用的类型,并像其名称所说明的那样调用CreateInstance函数。
但CallCreatelnstance仍使客户和其组件的实现之间是种紧密的联系。因为客户并不需要知道实现组件的DLL的名称。按照我们的设想,用户应该能够将组件从一个DLL中移到另外一个DLL中,或将此DLL从某个目录移到另外一个目录中。在第6章和第7章中我们将讨论一种更为一般的、更为灵活的创建组件的方法,以便将客户和组件彻底地分开。在第7章中,CallCreatelnstance将被一个名为CoCrealelnstance的COM库函数所代替。但在此之前,我们将一直使用CallCreateInstance来创建组件。
使用DLL的原因
为什么可以使用DIL来实现组件呢?其原因在于DLL可以共享它们所链入的应用程序的地址空间。前面我们讲过,客户和组件是通过接口进行交互的。一个接口实际上是一个指向函数的指针列表(vbl)。组件将为vtbl分配内存并用每一个函数的地址来初始化此表格。为使用vtbl,客户应该能够访问组件为其vbl所分配的内存。同时它还必须能够理解组件放人到vibl中的各个地址。在Windows中,由于动态链接库与客户使用的是同一地址空间,因此客户访问vtbl 是不成问题的。
在Windows中一个正在被执行的程序被称作是一个进程。每一个应用程序(EXE)都将以一个单独的进程运行,每一个进程都有一个4GB的地址空间。一个进程中的一个地址同另外一个进程中的某个地址是不同的。由于指针是在不同的地址空间中起作用的,因此不能将一个指针从一个进程传到另外一个进程。为理解这一点,可以类比一下街道地址。例如,对于地址369 Peachtree St.,它可能位于亚特兰大的一个商业大街上,也可能是位于西雅图的某片咖啡馆中。若没有指定城市名称,这个地址实际上是没有意义的。计算机中的进程就相当于是城市。两个不同进程中的指针可以包含相同的地址值,但它们实际上指向的是不同的物理内存。
所幸的是,动态链接库将驻留在所链接的应用程序的地址空间中,由于DLL和 EXE共享同一进程,因此它们也可以共享同一地址空间。由于这个原因,DLL经常也被称作是进程中服务器(in-process server)。在第10章中我们将讨论以EXE方式实现的“进程外服务器”,或“本地及远程服务器”。进程外服务器具有与其客户不同的地址空间,但我们将仍然使用DLL来帮助实现进程外服务器同其客户之间的交流。图5-1显示了DLL是如何映射到其客户的地址空间的。
此处的关键之处在于,当客户得到组件的一个接口指针时,连接客户和组件的唯一中介是接口的二进制结构。当客户查询组件的某个接口时,它所请求的实际上是具有特定格式的一块内存。当组件返回一个接口指针时,它告诉客户的实际上是此块内存的地址。由于接口是在客户和组件都能够访问的内存中,因此这种情况实际上与当客户和组件在同一EXE文件中是相同的。对于客户而言,唯一的区别在于在静态链接及动态链接的况下它获取接口指针的方式不同。
客户和组件的划分
在本节中我们将讨论如何将程序清单4-1给出的代码进行划分。然后我们再来看一下各部分的内容。
图5-2显示了包含有单个客户及单个组件的文件名称。
在此,客户将在CLIENT1.CPP中实现。客户文件还包括CREATE.H并将同CREATE.CPP一块链接起来。这两个文件将把创建包含在DLL中组件的过程封装起来。(CREATE.CPP如程序清单5-2所列)在第7章中我们将看到这些文件将被COM库中的函数所取代。
组件在此是在一个名为CMPNT1.CPP的文件中实现的。由于动态链接需要一个列出从DLL中输出的函数名称的模块定义文件,因此在组件文件中还包括一个名为CMPNT1.DEF的文件。关于CMPNT1.DEF,可参见程序清单5-1。
组件和客户共享了两个文件。其中之一IFACE.H包含 CMPNT1 支持的所有接口的声明,其中还包含这些接口的接口ID声明。关于这些接口 ID的定义则在文件GUIDS.CPP中。对GUID的详细讨论请参阅下一章,在此读者可以不必深究。
程序清单
程序清单5-3列出了客户的实现代码。
在此,客户首先要求用户输入DLL的名称,然后将此名称传递给CallCreatelnstance。CallCreatelnstance 将调用指定DLL中输出的CreateInstance函数来建立相应的组件。
// 程序清单5-3 此客户询问包含待建立的DIL的名称。然后它将装载此DIL,在建立相应的组件之后使用组件中的接口。
//client1.cpp
#include <iostream>
#include <objbase.h>
using namespace std;
void trace(const char* msg)//定义了一个名为 trace 的函数,用于输出消息。这个函数用于在运行时打印调试信息。
{
cout << "Client 1: \t" << msg << endl;
}
typedef IUnknown* (*CreateInstanceFunc)();
IUnknown* CallCreateInstance(const char* dllName) {//定义了 CallCreateInstance 函数,用于加载指定的 DLL 文件。
HMODULE hModule = LoadLibraryA(dllName);//使用 LoadLibraryA 函数加载 DLL 文件
if (!hModule) //如果加载失败,则打印错误信息并返回 nullptr
{
trace("Failed to load the DLL.加载DLL失败。");
return nullptr;
}
CreateInstanceFunc CreateInstance = (CreateInstanceFunc)GetProcAddress(hModule, "CreateInstance");
if (!CreateInstance) {
trace("Failed to get the CreateInstance function.获取CreateInstance函数失败。");
FreeLibrary(hModule);
return nullptr;
}
return CreateInstance();//调用 CreateInstance 函数并返回一个 IUnknown 接口指针
}
// Clientl
int main()
{
HRESULT hr;
// Get the name of the component to use. 获取要使用的组件的名称。
char name[40];
cout << "Enter the filename of a component to use[cmont ? .dil]:输入要使用的组件的文件名[cmont?. dil]:";
cin >> name;
cout << endl;
// Create component by calling the CreateInstance function in the DLL. 通过调用DLL中的CreateInstance函数来创建组件。
trace("Get an IUnknown pointer.获取一个IUnknown指针。");
IUnknown* plonknown = CallCreateInstance(name);//调用 CallCreateInstance 函数,尝试创建组件对象。
if (pIUnknown == NULL)
{
trace("CallCreateInstance Failed.CallCreateInstance失败。");
return 1;//如果返回的指针为 NULL,表示创建失败,输出错误信息并退出程序。
}
trace("Get interface IX.获取接口IX。");
IX* pIX = NULL;
hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);//使用 QueryInterface 方法查询 IX 接口,若成功pIX 将指向该接口
if (SUCCEEDED(hr))
{
pIX->Release();
pIX->Fx();// Use interface Ix.使用接口Ix。
trace("Succeeded getting IX.成功得到IX。");
}
else
{
trace("Could not get interface IX.无法获取接口IX。");
}
trace("Release IUnknown interface.发布IUnknown接口。");
pIUnknown->Release();
return 0;
}
程序清单5-4给出了实现组件的代码。
除了CreateInstance加上了extemn“C”标记外,此组件基本上同前面关于此组件的代码是一样的。只不过现在它是在一个单独的文件CMPNT1.CPP中。在编译此文件时加上了LLD选项,并同程序清单5-1所示的CMP-NT1.DEF一块链接起来了。
// 程序清单5-4 在一个单独的文件中所实现的组件。
// cmpent1.cpp
// To compile, use: cl /LD Cmpntl.cpp GUIDs.cpp UUID.lib Cmontl.def
#include <iostream>
#include <objbase.h>
#include "Iface.h"
using namespace std;
void trace(const char* msg)
{
cout << "Component l:\t" << msg << endl;
}
// Component
class CA : public IX
{
// tunknown implementation 可知实现
virtual HRESULT _stdcall QueryInterface(const IID& iid, void** pov);
virtual ULONG _stdcall AddRef();
virtual ULONG _stdcall Release();
// Interface IX implementation 接口IX实现
virtual void _stdcall Fx()
{
cout << "Fx" << endl;
}
public:
CA() : m_cRef(0) {}
~CA() { trace("Destroy seit."); }
private:
long m_cRef;
};
HRESULT _stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown)
{
trace("Return pointer to tUnknown.返回指向tUnknown的指针。");
*ppv = static_cast <IX*>(this);
}
else if (iid == IID_IX)
{
trace("Return pointer to Ix.返回指向Ix的指针。");
*ppv = static_cast<IX*>(this);
}
else
{
trace("Interface not supported.不支持接口。");
*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" IUnknown * CreateInstance()
{
IUnknown* pI = static_cast<IX*>(new CA);
pI->AddRef();
return pI;
}
下面就是两个共享的文件IFACE.H和GUIDS.CPP。在IFACE.H中声明了客户和组件所使用的所有接口。
// 程序清单5-5 接口声明。
//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 GUIDs的转发引用
extern "C"
{
extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;
}
可以看到,接口和组件仍然使用了IX、IY和IZ接口。在IFACE.H的末尾声明了这些接口的ID。关于接口ID我们将在下一章中详细讨论。这些接口ID的定义在文件GUIDS.CPP中,如程序清单5-6所列。
// 程序清单5-6 在GUIDS.CPP中定义了接口ID。客户和组件将与此GUIDS.CPP链接起来。
// guids.cpp
// guids.cpp-Interface IDs
# include <objbase.h>
extern"C"
{
// {32bb8320-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IX =
{ 0x32bb8320, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };
//{32bb8321-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IY =
{ 0x32bb8321,0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xe7, 0xb2, 0xd6, 0x82} };
//{32bb8322-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IZ =
{ 0x32bb8322,0xb41b,0x11cf,
{ 0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82 } };
// The extern is required to allocate memory for C++ constants,
}