C++
语言毕竟能和窗口DLL
可和平共处.
介绍
自从窗口
的开始阶段
,动态链接库
(DLL
)就是窗口
平台的一个组成部分
.动态链接库
,允许在一个独立的模块
中封装一系列的功能函数
,然后以一个显式的C函数列表
供外部用户
使用.
在上个世纪80
年代,当窗口DLL
面世时,对广大程序员而言只有C语言
是切实可行
的开发手段
.所以,窗口DLL
很自然地以C函数和数据
向外暴露功能
.
从本质
来说,一个DLL
可由任意语言
实现,但是为了使DLL
在其它语言和环境
下使用,一个DLL
接口必须回退
到C语言
.
而由一个C++
编译器产生的二进制代码
,并不可以其它C++
编译器兼容.再者,在同一个编译器
但不同版本
的二进制代码
也是互不兼容
的.
所有这些,导致从一个DLL
中导出一个C++
类简直就是一个冒险
.
这里演示几种
从一个DLL
模块中导出C++
类的方法.源码
演示了导出虚构的Xyz
对象的不同技巧
.Xyz
对象非常简单,只有一个函数:Foo
.
下面是Xyz
对象的图解
:
Xyz
int Foo(int)
在一个DLL
里实现Xyz
对象,该DLL
可按分布式系统
供大范围
的客户使用.一个用户
可按下面三个方式
调用Xyz
的功能:
1,使用纯C
2,使用一个普通的C++
类
3,使用一个抽象的C++
接口
源码包含两个工程
:
XyzLibrary
,一个DLL
工程
XyzExecutable
,一个Win32
使用"XyzLibrary.dll"
的控制台程序
.
XyzLibrary
工程使用下列方便的宏
导出它的代码
:
#if defined(XYZLIBRARY_EXPORT)//`DLL`内部
# define XYZAPI __declspec(dllexport)
#else//`DLL`外部
# define XYZAPI __declspec(dllimport)
#endif//`XYZLIBRARY_EXPORT`
仅在XyzLibrary
工程定义XYZLIBRARY_EXPORT
标识,因此在生成DLL
时,按__declspec(dllexport)
扩展XYZAPI
宏而,而在生成客户程序
时按__declspec(dllimport)
扩展.
C语言方式
句柄
经典的C语言方式
面向对象编程,就是使用晦涩的指针
,比如句柄
.一个用户
可使用一个函数
创建一个对象
.实际上该函数
返回的是该对象
的一个句柄
.
接着用户
可调用该对象相关的各种操作函数
,只要该函数可按它的一个参数
接受该句柄
.一个很好的示例
就是,在Win32
窗口相关的API
中句柄的习惯是,使用一个窗柄
句柄来代表一个窗口
.
而Xyz
对象,如下导出一个C接口
:
typedef tagXYZHANDLE {} * XYZHANDLE;//创建一个`Xyz`对象实例的函数
XYZAPI XYZHANDLE APIENTRY GetXyz(VOID);//调用`Xyz.Foo`函数
XYZAPI INT APIENTRY XyzFoo(XYZHANDLE handle, INT n);//释放`Xyz`实例和消费的资源
XYZAPI VOID APIENTRY XyzRelease(XYZHANDLE handle);//在`WinDef.h`头文件中按`__stdcall`定义`APIENTRY`.
下面是一个客户
调用的C代码
:
#include "XyzLibrary.h"
...
/*创建`Xyz`实例*/
XYZHANDLE hXyz = GetXyz();
if(hXyz)
{/*调用`Xyz.Foo`函数*/XyzFoo(hXyz, 42);/*析构`Xyz`实例,并释放已取得的资源.*/XyzRelease(hXyz);hXyz = NULL;
}
这样,一个DLL
必须有显式
的构建和删除对象
的函数.
调用协议
对所有的导出函数
,记住它们的调用协议
是重要的.忘记添加调用协议
是非常普遍的错误
.只要客户的调用协议
和DLL
的调用协议
匹配,一切都能运行
.
但是,一旦客户
改变了它的调用协议
,就会产生难以察觉
的运行时
错误.XyzLibrary
工程使用一个在"WinDef.h"
该头文件里,按__stdcall
定义的APIENTRY
宏.
异常缺点
由DLL
的用户,取正确对象的合适的方法
.比如下面代码片断
,编译器不能捕捉
到其中的错误
:
/*`void*GetSomeOtherObject(void)`是别的地方定义的一个函数*/
XYZHANDLE h = GetSomeOtherObject();
/*啊!错误:在错误的对象实例上调用`Xyz.Foo`函数*/
XyzFoo(h, 42);
很麻烦!
C++
天然的方式:导出一个类
在窗口
平台上,几乎每一个现代的编译器
都支持从一个DLL
中导出一个类
.导出一个类
和导出
一个C函数
类似.
只要在类名
前使用__declspec(dllexport/dllimport)
关键字来指定导出整个类
,或在函数声明
前指定导出特定的成员函数
,就可以.
如下:
//导出包括它的函数和成员的整个`CXyz`类
class XYZAPI CXyz
{
public:int Foo(int n);
};//只导出`CXyz::Foo`函数
class CXyz
{
public:XYZAPI int Foo(int n);
};
导出整个类
或它们的方法
时,不必显式
指定一个调用协议
.默认,C++
编译器使用__thiscall
作为类成员函数的调用协议
.
然而,不同编译器
有不同
的命名混杂法则
,导出的C++
类只能来同一类型
的同一版本
的编译器
.
只有MSVisualC++
编译器可使用该DLL.DLL
和客户代码
,只有在同一版本
的MSVC++
编译器才能确保调用者
和被调的装饰名
匹配.
如下客户
使用Xyz
对象的示例:
#include "XyzLibrary.h"
...//客户使用`Xyz`对象按一个规则`C++`类.
CXyz xyz;
xyz.Foo(42);
导出的C++
类的用法
和其它C++
类的用法
几乎是一样的…
客户代码
和DLL
都必须和同一版本
的CRT
动态连接在一起.为了可在模块
间修复CRT
资源的纪录,这一步
是必需的.
C++
成熟的方法:使用抽象接口
一个C++
抽象接口,成功完成了两全其美:对对象
而言,独立于编译器的规则
的接口
及方便
的面向对象方式
的调用函数
.
要提供一个接口声明的头文件
,同时实现一个可返回最新
创建的对象实例
的工厂函数
.只要用__declspec(dllexport/dllimport)
指定该工厂函数
就可以了.
不需要额外
指定接口
.
//无需额外指定`Xyzobject`的`抽象接口`.
struct IXyz
{virtual int Foo(int n) = 0;virtual void Release() = 0;
};//创建`Xyz`对象实例的工厂函数
extern "C" XYZAPI IXyz* APIENTRY GetXyz();
上面代码片断
中,按extern XYZAPI
声明GetXyz
工厂函数.用来避免装饰函数名
.
这样,在外部按一个普通的C函数
对待该函数
.如下使用抽象接口
:
#include "XyzLibrary.h"
...
IXyz* pXyz = ::GetXyz();
if(pXyz)
{pXyz->Foo(42);pXyz->Release();pXyz = NULL;
}
因为C++
天生支持面向对象
.微软很明显按产业COM
的重量级的工具
开发它.作为COM
技术的物主,微软已确保COM
的二进制标准
和它们拥有的在VC++
编译器实现的C++
对象模型可以最小的成本
实现匹配.