原文作者:Alex Blekhman<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
翻译:朱金灿
原文来源:
http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx
译文来源:http://blog.csdn.net/clever101
C++语言毕竟能和Windows DLLs能够和平共处。
介绍
自从Windows的开始阶段动态链接库(DLL)就是Windows平台的一个组成部分。动态链接库允许在一个独立的模块中封装一系列的功能函数然后以一个显式的C函数列表提供外部使用者使用。在上个世纪80年代,当Windows DLLs面世时,对于广大开发者而言只有C语言是切实可行的开发手段。所以, Windows DLLs很自然地以C函数和数据的形式向外部暴露功能。从本质来说,一个DLL可以由任何语言实现,但是为了使DLL用于其它的语言和环境之下,一个DLL接口必须后退到最低要求的母体——C语言。
使用C接口并不自动意味一个开发者应该应该放弃面向对象的开发方式。甚至C接口也能用于真正的面向对象编程,尽管它有可能被认为是一种单调乏味的实现方式。很显然世界上使用人数排第二的编程语言是C++,但它却不得不被DLL所诱惑。然而,和C语言相反,在调用者和被调用者之间的二进制接口被很好的定义并被广泛接受,但是在C++的世界里却没有可识别的应用程序二进制接口。实际上,由一个C++编译器产生的二进制代码并不能被其它C++编译器兼容。再者,在同一个编译器但不同版本的二进制代码也是互不兼容的。所有这些导致从一个DLL中一个C++类简直就是一个冒险。
这篇文章就是演示几种从一个DLL模块中导出C++类的方法。源码演示了导出虚构的Xyz对象的不同技巧。Xyz对象非常简单,只有一个函数:Foo。
下面是Xyz对象的图解:
Xyz对象在一个DLL里实现,这个DLL能作为一个分布式系统供范围很广的客户端使用。一个用户能以下面三种方式调用Xyz的功能:
- 使用纯C
- 使用一个规则的C++类
- 使用一个抽象的C++接口
源码(译注:文章附带的源码)包含两个工程:
- XyzLibrary – 一个DLL工程
- XyzExecutable – 一个Win32 使用"XyzLibrary.dll"的控制台程序
XyzLibrary工程使用下列方便的宏导出它的代码:
-
#ifdefined(XYZLIBRARY_EXPORT)//insideDLL
-
#defineXYZAPI__declspec(dllexport)
-
#else//outsideDLL
-
#defineXYZAPI__declspec(dllimport)
-
#endif//XYZLIBRARY_EXPORT
XYZLIBRARY_EXPORT
标识符仅仅在
XyzLibrary工程定义,因此在XYZAPI
宏在
DLL
生成时被扩展为
__declspec(
dllexport)
而在客户程序生成时被扩展为
__declspec(
dllimport)
。
C
语言方式
句柄
经典的C语言方式进行面向对象编程的一种方式就是使用晦涩的指针,比如句柄。一个用户能够使用一个函数创建一个对象。实际上这个函数返回的是这个对象的一个句柄。接着用户能够调用这个对象相关的各种操作函数只要这个函数能够接受这个句柄作为它的一个参数。一个很好的例子就是在Win32窗口相关的API中句柄的习惯是使用一个HWND句柄来代表一个窗口。虚构的Xyz对象通过下面这样一种方式导出一个C接口:
-
typedeftagXYZHANDLE{}*XYZHANDLE;
-
-
XYZAPIXYZHANDLEAPIENTRYGetXyz(VOID);
-
-
XYZAPIINTAPIENTRYXyzFoo(XYZHANDLEhandle,INTn);
-
-
XYZAPIVOIDAPIENTRYXyzRelease(XYZHANDLEhandle);
-
下面是一个客户端调用的C代码:
-
#include"XyzLibrary.h"
- ...
-
- XYZHANDLEhXyz=GetXyz();
-
if(hXyz)
- {
-
- XyzFoo(hXyz,42);
-
- XyzRelease(hXyz);
- hXyz=NULL;
- }
使用这种方式,一个DLL必须提供显式的对象构建和删除函数。
调用协定
对于所有的导出函数记住它们调用协定是重要的。对于很多初学者来说忘记添加调用协定是非常普遍的错误。只要客户端的调用协定和DLL的调用协定匹配,一切都能运行。但是,一旦客户端改变了它的调用协定,开发者将会产生一个难以察觉的直到运行时才发生的错误。XyzLibrary工程使用一个APIENTRY
宏,这个宏在
"WinDef.h"这个头文件里被定义为__stdcall。
异常安全性
在DLL范围内不允许发生C++异常。在一段时间内,C语言不识别C++的异常,并且不能正确处理它们。假如一个对象的方法需要报告一个错误,这时一个返回码需要用到。
优点
l 一个DLL能被最广泛的合适的开发者所使用。几乎每一种现代编程语言都支持纯C函数的互用性。
l 一个DLL的C运行时库和它的客户端是互相独立的。因为资源的获取和释放完全发生在DLL模块的内部,所以一个客户端不受一个DLL的C运行时库选择的影响。
缺点
l 获取正确对象的合适的方法的责任落在DLL的使用者的肩上。比如在下面的代码片断,编译器不能捕捉到其中发生的错误:
缺点
l 获取正确对象的合适的方法的责任落在DLL的使用者的肩上。比如在下面的代码片断,编译器不能捕捉到其中发生的错误:
-
- XYZHANDLEh=GetSomeOtherObject();
-
- XyzFoo(h,42);
l
显式要求创建和摧毁一个对象的实例。其中特别烦人的是对象实例的删除。客户端必须极仔细地在一个函数的退出点调用XyzRelease
函数。假如开发者忘记调用
XyzRelease
函数,那时资源就会泄露,因为编译器不能跟踪一个对象实例的生命周期。那些支持析构函数或垃圾收集器的语言通过在
C
接口上作一层封装有助于降低这个问题发生的概率。
l
假如一个对象的函数返回或接受其它对象作为参数,那时
DLL
作者也就不得不为这些对象提供一个正确的
C
接口。假如退回到最大限度的复用,也就是
C
语言,那么只有以字节创建的类型(如
int, double, char*
等等)可以作为返回类型和函数参数
C++
天然的方式:导出一个类
在
Windows
平台上几乎每一个现代的编译器都支持从一个
DLL
中导出一个类。导出一个类和导出一个
C
函数非常相似。用那种方法一个开发者被要求做就是在类名之前使用
__declspec(dllexport/dllimport)
关键字来指定
假如整个类都需要被导出,或者在指定的函数声明前指定假如只是特定的类函数需要被导出。这儿有一个代码片断:
-
-
classXYZAPICXyz
- {
-
public:
-
intFoo(intn);
- };
-
-
-
classCXyz
- {
-
public:
-
XYZAPIintFoo(intn);
- };
在导出整个类或者它们的方法没有必要显式指定一个调用协定。
根据预设,C++编译器使用__thiscall
作为类成员函数的调用协定。然而,由于不同的编译器具有不同的命名修饰法则,导出的C++类只能用于同一类型的同一版本的编译器。这儿有一个MS Visual C++编译器的命名修饰法则的应用实例:
注意这里修饰名是怎样不同于C++原来的名字。下面是屏幕截图显示的是通过使用Dependency Walker工具对同一个DLL的修饰名进行破译得到的:
只有MS Visual C++编译器能使用这个DLL.DLL和客户端代码只有在同一版本的MS Visual C++编译器才能确保在调用者和被调用者修饰名匹配。这儿有一个客户端代码使用Xyz对象的例子:
-
#include"XyzLibrary.h"
- ...
-
- CXyzxyz;
- xyz.Foo(42);
正如你所看到的,导出的C++类的用法和其它任何C++类的用法几乎是一样的。没什么特别的。
重要事项:使用一个导出C++类的DLL和使用一个静态库没有什么不同。所有应用于有C++代码编译出来的静态库的规则完全适用于导出C++类的DLL。
所见即所得
一个细心的读者必然已经注意到
Dependency Walker工具显示了额外的导出成员,那就是CXyz& CXyz::
operator =(
const CXyz&)
赋值操作符。在工作你所看到的正是
C++
的收入(译注:我估计这是原文作者幽默的说法,意思是你没有定义一个
=
赋值操作符,而编译器帮你自动定义一个,不是收入是什么?)。根据
C++
标准,每一个类有四个指定的成员函数:
- 默认构造函数
- 拷贝构造函数
- 析构函数
- 赋值操作符 (operator =)
假如类的作者没有声明同时没有提供这些成员的实现,那么
C++
编译器会声明它们,并产生一个隐式的默认的实现。在
CXyz
类,编译器断定它的默认构造函数,拷贝构造函数和析构函数都毫无意义,经过优化后把它们排除掉了。而赋值运算符在优化之后还存活并从
DLL
中导出。
重要事项:使用
__declspec(
dllexport)
来指定类导出来告诉编译器来尝试导出任何和类相关的东西。它包括所有类的数据成员,所有类的成员函数(或者显式声明,或者由编译器隐式生成),所有类的基类和所有它们的成员。考虑:
-
classBase
- {
- ...
- };
-
classData
- {
- ...
- };
-
-
class__declspec(dllexport)Derived:
-
publicBase
- {
- ...
-
private:
-
Datam_data;
- };
在上面的代码片断,编译器会警告你没有导出基类和类的数据成员。所以,为了成功导出一个类,一个开发者被要求导出所有相关基类和所有类的已定义的数据成员。这个滚雪球般的导出要求是一个重大缺点。这也是为什么,比如,导出派生自
STL
模板类或者使用
STL
模板类对象作为数据成员是非常困难和令人生厌的。比如一个
STL
容器比如
std::map
<>实例可能要求导出数十个额外的内部类。
异常安全性
一个导出的
C++
类可能会在没有任何错误发生的情况下抛出异常。因为一个
DLL
和它的客户端使用同一版本的同一类型的编译器的事实,
C++
异常将在超出
DLL
的范围进行捕捉和抛出好像
DLL
没有分界线一样。记住,使用一个带有导出
C++
代码和使用带有相同代码的静态库是完全一样的。
优点
l
一个导出的
C++
类和其它任何
C++
类的用法是一样的
l
客户端能毫不费力地捕捉在
DLL
发生的异常
l
当在一个
DLL
模块内有一些小的代码改动时,其它模块也不用重新生成。这对于有着许多复杂难懂代码的大工程是非常有用的。
l
在一个大工程中按照业务逻辑分成不同的
DLL
实现可以被认为真正的模块划分的第一步。总的来说,它是使工程达到模块化值得去做的事
缺点
l
从一个
DLL
中导出
C++
类在它的对象和使用者需要保持紧密的联系。
DLL
应该被视作一个带有考虑到代码依赖的静态库。
l
客户端代码和
DLL
都必须和同一版本的
CRT
(译注:
C
运行时库)动态连接在一起。为了能够在模块之间修正
CRT
资源的纪录,这一步是必需的。假如一个客户端和
DLL
连接到不同版本的
CRT
,或者静态连接到
CRT
,那么在一个
CRT
实例申请的资源有可能在另一个
CRT
实例中释放。它将损坏
CRT
实例的内在状态并企图操作外部资源,并很可能导致运行失败。
l
客户端代码和
DLL
必须在异常处理和产生达成一致,同时在编译器的异常设置也必须一致
l
导出
C++
类要求同时导出这个类的所有相关的东西,包括:所有它的基类、所有类定义的用到的数据成员等等。
C++
成熟的方法:使用抽象接口
一个
C++
抽象接口(比如一个拥有纯虚函数和没有数据成员的
C++
类)设法做到两全其美:对对象而言独立于编译器的规则的接口以及方便的面向对象方式的函数调用。为达到这些要求去做的就是提供一个接口声明的头文件,同时实现一个能返回最新创建的对象实例的工厂函数。只有这个工厂函数需要使用
__declspec(dllexport/dllimport)
指定。接口不需要任何额外的指定。
- //Xyzobject的抽象接口
- //不要求作额外的指定
- structIXyz
- {
-
virtualintFoo(intn)=0;
-
virtualvoidRelease()=0;
- };
- //创建Xyz对象实例的工厂函数
- extern"C"XYZAPIIXyz*APIENTRYGetXyz();
在上面的代码片断中,工厂函数
GetXyz
被声明为
extern XYZAPI
。这样做是为了防止函数名被修饰(译注:如上面提到的导出一个
C++
类,其成员函数名导出后会被修饰)。这样,这个函数在外部表现为一个规则的
C
函数,并且很容易被和
C
兼容的编译器所识别。这就是当使用一个抽象接口时客户端代码看起来和下面一样:
-
#include"XyzLibrary.h"
- ...
- IXyz*pXyz=::GetXyz();
-
if(pXyz)
- {
- pXyz->Foo(42);
- pXyz->Release();
- pXyz=NULL;
- }
C++
不用为接口提供一个特定的标记以便其它编程语言使用(比如
C#
或
Java
)。但这并不意味
C++
不能声明和实现接口。设计一个
C++
的接口的一般方法是去声明一个没有任何数据成员的抽象类。这样,派生类可以继承这个接口并实现这个接口,但这个实现对客户端是不可见的。接口的客户端不用知道和关注接口是如何实现的。它只需知道函数是可用的和它们做什么。
内部机制
在这种方法背后的思想是非常简单的。一个由纯虚函数组成的成员很少的类只不过是一个虚函数表——一个函数指针数组。在
DLL
范围内这个函数指针数组被它的作者填充任何他认为必需的东西。这样这个指针数组在
DLL
外部使用就是调用接口的实际上的实现。下面是
IXyz
接口的用法说明图表。
上面的图表演示了IXyz接口被DLL和EXE模块二者都用到。在DLL模块内部,XyzImpl类派生自IXyz接口并实现它的方法。在EXE的函数调用引用DLL模块经过一个虚表的实际实现。
这种DLL为什么能和其它的编译器一起运行
EN-US
分享到:
相关推荐
一个简单的C++生成DLL(导出类),然后用控制台程序去调用它的示例,可以帮助你理解如何从DLL中导出类,并使用该类。 该项使用VS2010编译,低版本可能无法打开。
C++DLL中包含一个类,C#端要调用这个类的函数,重新封装这个类,来供C#端调用
C++ 导出dll接口函数 C++ 导出dll接口函数 C++ 导出dll接口函数
博客中用到的demo,[【cmake实战十】c++从动态库(dll)导出类](https://blog.csdn.net/junxuezheng/article/details/126908851)
在VC中在DLL中导出C++类
DLL导出类的三种方法,导出接口类,导出类指针,导出类,不建议使用导出类
一步步让你学在MFC中如何封装DLL,实现DLL的导出
Depends是一个的实用工具,扫描任何32位或64位Windows模块( EXE文件, DLL的,控件,系统等) ,并建立所有相关模块的层次树图。 可以查看C++导出DLL的导出函数信息,同时也可以查看EXE文件的依赖关系,是C++\C#...
在VC中的DLL类导出以后完全是可以被delphi调用的. 源码中有详细介绍和说明文档.
教你首次使用 MFC 扩展DLL(导出类)的Demo
DLL的大部分调用都是利用.h和.lib,即使动态调用DLL也只是调用其中的导出的函数,而不是导出的类。也许很多小伙伴会说我动态调用是多此一举,但这里主要是技术交流,让大家知道这样做也是可行的。注意:类成员函数...
Python中调用C++dll例子,使用python中的ctypes。
可导出为按键和TC等脚本调用的dll,扩展性强,无需担心跨越语言不能用
自定义的dll中包含自定义类,如何从dll中到处自定义类,具体查看源代码
该文档中有三个示例,分别讲述了如何从一个DLL中导出名称空间中的函数,名称空间中的类与名称空间中的全局变量。每一个示例包括头文件,C++文件,C++测试文件与makefile。该资源系本人所写,无积分下载,可以自由...