原文作者:Alex Blekhman
翻译:朱金灿
原文来源:
http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx
译文来源:http://blog.csdn.net/clever101/article/details/3034743
C++语言毕竟能和Windows DLLs能够和平共处。
自从Windows的开始阶段动态链接库(DLL)就是Windows平台的一个组成部分。
使用C接口并不自动意味一个开发者应该应该放弃面向对象的开发方式。
这篇文章就是演示几种从一个DLL模块中导出C++类的方法。源码演示了导出虚构的Xyz对象的不同技巧。Xyz对象非常简单,只有一个函数:Foo。
下面是Xyz对象的图解:
|
Xyz |
|
int Foo(int) |
Xyz对象在一个DLL里实现,这个DLL能作为一个分布式系统供范围很广的客户端使用。一个用户能以下面三种方式调用Xyz的功能:
- 使用一个规则的C++类
- 使用一个抽象的C++接口
源码(译注:文章附带的源码)包含两个工程:
- XyzLibrary – 一个DLL工程
- XyzExecutable – 一个Win32 使用"XyzLibrary.dll"的控制台程序
XyzLibrary工程使用下列方便的宏导出它的代码:
- #if defined(XYZLIBRARY_EXPORT) // inside DLL
- # define XYZAPI __declspec(dllexport)
- #else // outside DLL
- # define XYZAPI __declspec(dllimport)
- #endif // XYZLIBRARY_EXPORT
XYZLIBRARY_EXPORT标识符仅仅在XyzLibrary工程定义,因此在XYZAPI宏在DLL生成时被扩展为__declspec(dllexport)而在客户程序生成时被扩展为__declspec(dllimport)。
C语言方式
经典的C语言方式进行面向对象编程的一种方式就是使用晦涩的指针,比如句柄。虚构的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);
- // APIENTRY is defined as __stdcall in WinDef.
下面是一个客户端调用的C代码:
- #include "XyzLibrary
- /* 创建Xyz实例*/
- XYZHANDLE hXyz = GetXyz();
- if(hXyz)
- /* 调用 Xyz.Foo函数*/
- XyzFoo(hXyz, 42);
- /*析构 Xyz实例并释放已取得的资源. */
- XyzRelease(hXyz);
- hXyz = NULL;
使用这种方式,一个DLL必须提供显式的对象构建和删除函数。
对于所有的导出函数记住它们调用协定是重要的
异常安全性
在DLL范围内不允许发生C++异常。
l 一个DLL能被最广泛的合适的开发者所使用。几乎每一种现代编程语言都支持纯C函数的互用性。
l 一个DLL的C运行时库和它的客户端是互相独立的。因为资源的获取和释放完全发生在DLL模块的内部,所以一个客户端不受一个DLL的C运行时库选择的影响。
l 获取正确对象的合适的方法的责任落在DLL的使用者的肩上。比如在下面的代码片断,编译器不能捕捉到其中发生的错误:
l 获取正确对象的合适的方法的责任落在DLL的使用者的肩上。比如在下面的代码片断,编译器不能捕捉到其中发生的错误:
- /* void* GetSomeOtherObject(void)是别的地方定义的一个函数 */
- XYZHANDLE h = GetSomeOtherObject();
- /* 啊! 错误: 在错误的对象实例上调用Xyz.Foo函数*/
- XyzFoo(h, 42);
l 显式要求创建和摧毁一个对象的实例。
l 假如一个对象的函数返回或接受其它对象作为参数,那时DLL作者也就不得不为这些对象提供一个正确的C接口。假如退回到最大限度的复用,也就是C语言,那么只有以字节创建的类型(如int, double, char*等等)可以作为返回类型和函数参数
C++天然的方式:导出一个类
在Windows平台上几乎每一个现代的编译器都支持从一个DLL中导出一个类。这儿有一个代码片断:
- // 整个CXyz类被导出,包括它的函数和成员
- class XYZAPI CXyz
- public:
- int Foo(int n);
- // 只有 CXyz::Foo函数被导出
- class CXyz
- public:
- XYZAPI int Foo(int n);
在导出整个类或者它们的方法没有必要显式指定一个调用协定。然而,由于不同的编译器具有不同的命名修饰法则,导出的C++类只能用于同一类型的同一版本的编译器。这儿有一个MS Visual C++编译器的命名修饰法则的应用实例:
注意这里修饰名是怎样不同于C++原来的名字Dependency Walker 工具对同一个DLL的修饰名进行破译得到的:
只有MS Visual C++编译器能使用这个DLL.DLL和客户端代码只有在同一版本的MS Visual C++编译器才能确保在调用者和被调用者修饰名匹配。这儿有一个客户端代码使用Xyz对象的例子:
- #include "XyzLibrary
- // 客户端使用Xyz对象作为一个规则C++类.
- CXyz xyz;
- xyz.Foo(42);
正如你所看到的,导出的C++类的用法和其它任何C++类的用法几乎是一样的。没什么特别的。
重要事项:使用一个导出C++类的DLL和使用一个静态库没有什么不同。所有应用于有C++代码编译出来的静态库的规则完全适用于导出C++类的DLL。
所见即所得
一个细心的读者必然已经注意到Dependency Walker工具显示了额外的导出成员,那就是CXyz& CXyz::operator =(const CXyz&)赋值操作符。根据C++标准,每一个类有四个指定的成员函数:
- 默认构造函数
- 拷贝构造函数
- 赋值操作符 (operator =)
假如类的作者没有声明同时没有提供这些成员的实现,那么C++编译器会声明它们,并产生一个隐式的默认的实现。
重要事项:使用__declspec(dllexport)来指定类导出来告诉编译器来尝试导出任何和类相关的东西。考虑:
- class Base
- class Data
- // MS Visual C++ compiler 会发出C4275 warning ,因为没有导出基类
- class __declspec(dllexport) Derived :
- public Base
- private:
- Data m_data; // C4251 warning,因为没有导出数据成员.
在上面的代码片断,编译器会警告你没有导出基类和类的数据成员。
异常安全性
一个导出的C++类可能会在没有任何错误发生的情况下抛出异常。
优点
l 一个导出的C++类和其它任何C++类的用法是一样的
l 客户端能毫不费力地捕捉在DLL发生的异常
l 当在一个DLL模块内有一些小的代码改动时,其它模块也不用重新生成。这对于有着许多复杂难懂代码的大工程是非常有用的。
l 在一个大工程中按照业务逻辑分成不同的DLL实现可以被认为真正的模块划分的第一步。总的来说,它是使工程达到模块化值得去做的事
缺点
l 从一个DLL中导出C++类在它的对象和使用者需要保持紧密的联系。
l 客户端代码和DLL都必须和同一版本的CRT(译注:C运行时库)动态连接在一起。
l 客户端代码和DLL必须在异常处理和产生达成一致,同时在编译器的异常设置也必须一致
l 导出C++类要求同时导出这个类的所有相关的东西,包括:所有它的基类。
C++成熟的方法:使用抽象接口
一个C++抽象接口(比如一个拥有纯虚函数和没有数据成员的C++类)设法做到两全其美:对对象而言独立于编译器的规则的接口以及方便的面向对象方式的函数调用。
- // Xyz object的抽象接口
- // 不要求作额外的指定
- struct IXyz
- virtual int Foo(int n) = 0;
- virtual void Release() = 0;
- // 创建Xyz对象实例的工厂函数
- extern "C" XYZAPI IXyz* APIENTRY GetXyz();
在上面的代码片断中,工厂函数GetXyz被声明为extern XYZAPI。这就是当使用一个抽象接口时客户端代码看起来和下面一样:
- #include "XyzLibrary
- IXyz* pXyz = ::GetXyz();
- if(pXyz)
- pXyz->Foo(42);
- pXyz->Release();
- pXyz = NULL;
C++不用为接口提供一个特定的标记以便其它编程语言使用(比如C#或Java)。接口的客户端不用知道和关注接口是如何实现的。它只需知道函数是可用的和它们做什么。
内部机制
在这种方法背后的思想是非常简单的。下面是IXyz接口的用法说明图表。
上面的图表演示了IXyz接口被DLL和EXE模块二者都用到。
这种DLL为什么能和其它的编译器一起运行
简短的解释是:因为COM技术和其它的编译器一起运行。
难怪其它的编译器厂商都和微软采用相同的方式实现虚表的布局。
使用一个智能指针
为了确保正确的资源释放,一个虚接口提供了一个额外的函数来清除对象实例。这儿有一段演示带有IXyz接口的一个智能指针的用法的代码片断:
- #include "XyzLibrary
- #include "AutoClosePtr.h"
- typedef AutoClosePtr<IXyz, void, &IXyz::Release> IXyzPtr;
- IXyzPtr ptrXyz(::GetXyz());
- if(ptrXyz)
- ptrXyz->Foo(42);
- // 不需要调用ptrXyz->Release(). 智能指针将在析构函数里自动调用这个函数
不管怎样,使用智能指针将确保Xyz对象能正当地适当资源。
异常安全性
和COM接口一样不再允许因为任何内部异常的发生而导致资源泄露,抽象类接口不会让任何内部异常突破DLL范围。
优点:
l 一个导出的C++类能够通过一个抽象接口,被用于任何C++编译器
l 一个DLL的C运行库和DLL的客户端是互相独立的。
l 真正的模块分离能高度完美实现。结果模块可以重新设计和重新生成而不受工程的剩余模块的影响。
l 如果需要,一个DLL模块能很方便地转化为真正的COM模块。
缺点:
l 一个显式的函数调用需要创建一个新的对象实例并删除它。尽管一个智能指针能免去开发者之后的调用
l 一个抽象接口函数不能返回或者接受一个规则的C++对象作为一个参数。
STL模板类是怎样做的
C++标准模板库的容器(如vector,list或map)和其它模板并没有设计为DLL模块(以抽象类接口方式)。
总结
这篇文章讨论了几种从一个DLL模块中导出一个C++对象的不同方法。对每种方法的优点和缺点的详细论述也已给出。下面是得出的几个结论:
l 以一个完全的C函数导出一个对象有着最广泛的开发环境和开发语言的兼容性。
l 导出一个规则的C++类和以C++代码提供一个单独的静态库没什么区别。
l 定义一个无数据成员的抽象类并在DLL内部实现是导出C++对象的最好方法。
这篇文章,包括任何源码和文件,遵循The Code Project Open License (CPOL) 协议。
Alex Blekhman 职业:软件开发者
国籍:以色列