原文作者:Alex Blekhman
原文来源:
http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx
译文来源:http://blog.youkuaiyun.com/clever101
C++语言毕竟能和Windows DLLs能够和平共处。
介绍
使用C接口并不自动意味一个开发者应该应该放弃面向对象的开发方式。甚至C接口也能用于真正的面向对象编程,尽管它有可能被认为是一种单调乏味的实现方式。很显然世界上使用人数排第二的编程语言是C++,但它却不得不被DLL所诱惑。然而,和C语言相反,在调用者和被调用者之间的二进制接口被很好的定义并被广泛接受,但是在C++的世界里却没有可识别的应用程序二进制接口。实际上,由一个C++编译器产生的二进制代码并不能被其它C++编译器兼容。再者,在同一个编译器但不同版本的二进制代码也是互不兼容的。所有这些导致从一个DLL中一个C++类简直就是一个冒险。
这篇文章就是演示几种从一个DLL模块中导出C++类的方法。源码演示了导出虚构的Xyz对象的不同技巧。Xyz对象非常简单,只有一个函数:Foo。
下面是Xyz对象的图解:
Xyz |
int Foo(int) |
Xyz对象在一个DLL里实现,这个DLL能作为一个分布式系统供范围很广的客户端使用。一个用户能以下面三种方式调用Xyz的功能:
使用纯C
使用一个规则的C++类
使用一个抽象的C++接口
源码(译注:文章附带的源码)包含两个工程:
XyzLibrary – 一个DLL工程
XyzExecutable – 一个Win32 使用"XyzLibrary.dll"的控制台程序
XyzLibrary工程使用下列方便的宏导出它的代码:
#if defined(XYZLIBRARY_EXPORT) // inside DLL
#
#else // outside DLL
#
#endif
XYZLIBRARY_EXPORT标识符仅仅在XyzLibrary工程定义,因此在XYZAPI宏在DLL生成时被扩展为__declspec(dllexport)而在客户程序生成时被扩展为__declspec(dllimport)。
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);
// APIENTRY is defined as __stdcall in WinDef.h header.
下面是一个客户端调用的C代码:
#include "XyzLibrary.h"
...
XYZHANDLE hXyz = GetXyz();
if(hXyz)
{
}
使用这种方式,一个DLL必须提供显式的对象构建和删除函数。
调用协定
异常安全性
优点
缺点
XYZHANDLE h = GetSomeOtherObject();
XyzFoo(h, 42);
// 整个CXyz类被导出,包括它的函数和成员
class XYZAPI CXyz
{
public:
};
// 只有 CXyz::Foo函数被导出
//
class CXyz
{
public:
};
在导出整个类或者它们的方法没有必要显式指定一个调用协定。根据预设,C++编译器使用__thiscall作为类成员函数的调用协定。然而,由于不同的编译器具有不同的命名修饰法则,导出的C++类只能用于同一类型的同一版本的编译器。这儿有一个MS Visual C++编译器的命名修饰法则的应用实例:
#include "XyzLibrary.h"
...
// 客户端使用Xyz对象作为一个规则C++类.
CXyz xyz;
xyz.Foo(42);
正如你所看到的,导出的C++类的用法和其它任何C++类的用法几乎是一样的。没什么特别的。
重要事项:使用一个导出C++类的DLL和使用一个静态库没有什么不同。所有应用于有C++代码编译出来的静态库的规则完全适用于导出C++类的DLL。
所见即所得
默认构造函数
拷贝构造函数
析构函数
赋值操作符 (operator =)
class Base
{
};
class Data
{
};
// MS Visual C++ compiler 会发出C4275 warning ,因为没有导出基类
class __declspec(dllexport) Derived :
{
private:
};
异常安全性
一个导出的C++类可能会在没有任何错误发生的情况下抛出异常。因为一个DLL和它的客户端使用同一版本的同一类型的编译器的事实,C++异常将在超出DLL的范围进行捕捉和抛出好像DLL没有分界线一样。记住,使用一个带有导出C++代码和使用带有相同代码的静态库是完全一样的。
优点
缺点
C++成熟的方法:使用抽象接口
// Xyz object的抽象接口
// 不要求作额外的指定
struct IXyz
{
};
// 创建Xyz对象实例的工厂函数
extern "C" XYZAPI IXyz* APIENTRY GetXyz();
在上面的代码片断中,工厂函数GetXyz被声明为extern XYZAPI。这样做是为了防止函数名被修饰(译注:如上面提到的导出一个C++类,其成员函数名导出后会被修饰)。这样,这个函数在外部表现为一个规则的C函数,并且很容易被和C兼容的编译器所识别。这就是当使用一个抽象接口时客户端代码看起来和下面一样:
#include "XyzLibrary.h"
...
IXyz* pXyz = ::GetXyz();
if(pXyz)
{
}
C++不用为接口提供一个特定的标记以便其它编程语言使用(比如C#或Java)。但这并不意味C++不能声明和实现接口。设计一个C++的接口的一般方法是去声明一个没有任何数据成员的抽象类。这样,派生类可以继承这个接口并实现这个接口,但这个实现对客户端是不可见的。接口的客户端不用知道和关注接口是如何实现的。它只需知道函数是可用的和它们做什么。
内部机制
这种DLL为什么能和其它的编译器一起运行
难怪其它的编译器厂商都和微软采用相同的方式实现虚表的布局。毕竟,每个人都想支持COM技术,并做到和微软已存在的解决方法兼容。假设某个C++编译器不能有效支持COM,那么它注定会被Windows市场所抛弃。这就是为什么时至今日,通过一个抽象接口从一个DLL导出一个C++类能和Windows平台上过得去的编译器能可靠地运行在一起。
使用一个智能指针
为了确保正确的资源释放,一个虚接口提供了一个额外的函数来清除对象实例。手动调用这个函数令人厌烦并容易导致错误发生。我们都知道这个错误在C世界里这是一个很普遍的错误,因为在那儿开发者不得不记得释放显式函数调用获取的资源。这就是为什么典型的C++代码借助于智能指针使用RAII(资源获取即初始化)的习惯。XyzExecutable工程提供了一个例子,使用了AutoClosePtr模板。AutoClosePtr模板是一个最简单的智能指针,这个智能指针调用了一个类消灭一个实例的主观方法来代替delete操作符。这儿有一段演示带有IXyz接口的一个智能指针的用法的代码片断:
#include "XyzLibrary.h"
#include "AutoClosePtr.h"
...
typedef AutoClosePtr<IXyz, void, &IXyz::Release> IXyzPtr;
IXyzPtr ptrXyz(::GetXyz());
if(ptrXyz)
{
}
// 不需要调用ptrXyz->Release(). 智能指针将在析构函数里自动调用这个函数
不管怎样,使用智能指针将确保Xyz对象能正当地适当资源。因为一个错误或者一个内部异常的发生,函数会过早地退出,但是C++语言保证所有局部对象的析构函数能在函数退出之前被调用。
异常安全性
和COM接口一样不再允许因为任何内部异常的发生而导致资源泄露,抽象类接口不会让任何内部异常突破DLL范围。函数调用将会使用一个返回码来明确指示发生的错误。对于特定的编译器,C++异常的处理都是特定的,不能够分享。所以,在这个意义上,一个抽象类接口表现得十足像一个C函数。
优点:
缺点:
STL模板类是怎样做的
C++标准模板库的容器(如vector,list或map)和其它模板并没有设计为DLL模块(以抽象类接口方式)。有关DLL的C++标准是没有的因为DLL是一种平台特定技术。C++标准不需要出现在没有用到C++语言的其它平台上。当前,微软的Visual C++编译器能够导出和导入开发者显式以__declspec(dllexport/dllimport)关键字标识的STL类实例。编译器会发出几个令人讨厌的警告,但是还能运行。然而,你必须记住,导出STL模板实例和导出规则C++类是完全一样的,有着一样的限制。所以,在那方面STL是没什么特别的。
总结
这篇文章讨论了几种从一个DLL模块中导出一个C++对象的不同方法。对每种方法的优点和缺点的详细论述也已给出。下面是得出的几个结论:
授权
这篇文章,包括任何源码和文件,遵循The Code Project Open License (CPOL)协议。