c++ dll导出函数,c++导入dll
作者:亚历克斯布雷克曼
翻译:朱
原始来源:
http://www . code project . com/KB/CPP/how to _ export _ CPP _ class . aspx
资料来源:http://blog.csdn.net/clever101
毕竟C可以和Windows DLLs和平共处。
介绍
自Windows诞生以来,动态链接库(DLL)一直是Windows平台不可或缺的一部分。动态链接库允许将一系列功能函数封装在一个独立的模块中,然后以一个显式的C函数列表提供给外部用户。在Windows DLLs问世的80年代,对于开发者来说,只有C语言是可行的开发工具。因此,Windows DLLs很自然地将其功能以C函数和数据的形式对外公开。本质上,DLL可以用任何语言实现,但是为了使DLL可以在其他语言和环境中使用,DLL接口必须退回到最低要求的父语言——C。
使用C接口并不自动意味着开发人员应该放弃面向对象的开发方法。即使C接口也可以用于真正的面向对象编程,尽管它可能被认为是一种繁琐的实现。显然,世界上第二常用的编程语言是C,但它不得不被DLL所诱惑。然而,与C语言相反,调用者和被调用者之间的二进制接口被很好地定义并被广泛接受,但在C世界中没有可识别的应用程序二进制接口。实际上,一个C编译器生成的二进制代码与其他C编译器是不兼容的。此外,同一编译器中不同版本的二进制代码互不兼容。这一切导致一个C类从一个DLL,这简直是一个冒险。
这篇文章演示了几种从DLL模块中导出C类的方法。源代码演示了导出虚构的Xyz对象的不同技巧。Xyz对象很简单,只有一个函数:Foo。
以下是Xyz对象的插图:
Xyz对象是在一个DLL中实现的,它可以作为一个分布式系统被广泛的客户端使用。用户可以通过以下三种方式调用Xyz的函数:
使用规则的C类
使用抽象C接口。
源代码包括两个项目:
XYZ图书馆——一个DLL项目
XYZ可执行文件在Win32中使用“XyzLibrary.dll”的控制台程序
XYZ项目使用以下方便的宏来导出其代码:
# if defined(XYZ library _ EXPORT)//inside dll
# defineXYZAPI _ _ declspec(dll export)
#else//outsideDLL
# defineXYZAPI _ _ declspec(dllimport)
#endif//XYZLIBRARY_EXPORT
XYZ _导出标识符只在XyzLibrary项目中定义,所以在生成DLL时,XYZAPI宏扩展为__declspec(dllexport),在生成客户端程序时,扩展为__declspec(dllimport)。
c语言模式
处理
经典C语言中面向对象编程的一种方式是使用晦涩的指针,比如句柄。用户可以使用函数创建对象。实际上,这个函数返回了这个对象的句柄。然后用户可以调用与这个对象相关的各种操作函数,只要这个函数可以接受这个句柄作为它的一个参数。一个很好的例子是在与Win32窗口相关的API中使用HWND句柄来表示窗口的习惯。的虚构Xyz对象以下列方式导出到C接口:
//创建Xyz对象实例的函数
XYZAPIXYZHANDLEAPIENTRYGetXyz(VOID);
//调用Xyz。Foo函数
xyzapintapientryxyzfoo(XYZHANDLEhandle,INTn);
//释放Xyz实例和占用的资源
XYZAPIVOIDAPIENTRYXyzRelease(XYZHANDLEhandle);
//apientrysdefinedas _ _ stdcallinwindef . h header。
下面是一个客户端调用的C代码:
呼叫协议
对于所有导出的函数,记住它们的调用约定很重要。忘记添加调用合约是很多新手的通病。只要客户端的调用协定与DLL的匹配,一切都可以运行。然而,一旦客户端改变了它的调用协议,开发人员就会产生一个直到运行时才会发生的察觉不到的错误。XYZ项目使用APIENTRY宏,该宏在头文件“WinDef.h”中定义为__stdcall。
异常安全
DLL范围内不允许c异常。有一段时间,C语言没有识别出C的异常,无法正确处理。如果一个对象的方法需要报告一个错误,那么需要使用一个返回代码。
优势
DLL可以被最广泛的合适的开发者使用。几乎所有现代编程语言都支持纯C函数的互操作性。
DLL的C运行时库和它的客户端是相互独立的。因为资源的获取和释放完全发生在DLL模块内部,所以客户端不受DLL的C运行时库选择的影响。
劣势
获取正确对象的正确方法的责任落在DLL用户的肩上。例如,在下面的代码片段中,编译器无法捕捉其中出现的错误:
劣势
获取正确对象的正确方法的责任落在DLL用户的肩上。例如,在下面的代码片段中,编译器无法捕捉其中出现的错误:
/* void/* void * GetSomeOtherObject(void)是在别处定义的函数*/
XYZHANDLEh=GetSomeOtherObject();
/*啊!错误:调用Xyz。错误对象实例上的Foo函数*/
XyzFoo(h,42);
l明确要求创建和销毁对象的实例。尤其讨厌的是删除对象实例。客户端必须非常小心地在函数的出口点调用XyzRelease函数。如果开发人员忘记调用XyzRelease函数,资源就会泄漏,因为编译器无法跟踪对象实例的生命周期。那些支持析构函数或垃圾收集器的语言,通过在C接口上做一层封装,有助于降低出现这个问题的概率。
如果一个对象的函数返回或接受其他对象作为参数,那么DLL作者必须为这些对象提供一个正确的C接口。如果回归到最大复用,也就是C语言,那么只有字节创建的类型(比如int,double,char*等。)可以用作返回类型和函数参数。
c自然方式:导出一个类。
几乎Windows平台上的所有现代编译器都支持从DLL导出类。导出一个类与导出一个C函数非常相似。这样,开发人员需要在类名前使用__declspec(dllexport/dllimport)关键字来指定是否需要导出整个类,或者指定在声明指定的函数之前是否只需要导出特定的类函数。下面是一段代码:
当导出整个类或它们的方法时,不需要显式指定调用协定。默认情况下,C编译器使用__thiscall作为类成员函数的调用约定。但是由于不同的编译器有不同的命名修饰规则,导出的C类只能用于同类型同版本的编译器。下面是MS Visual C编译器命名修改规则的一个应用实例:
请注意此处修改后的名称与c的原始名称有何不同。下面的屏幕截图显示,它是通过使用Dependency Walker工具解密同一个DLL的修饰名而获得的:
只有MS Visual C编译器可以使用此DLL.DLL和客户端代码。只有相同版本的MS Visual C编译器才能确保调用方和被调用方的修饰名匹配。下面是一个使用Xyz对象的客户端代码示例:
如你所见,导出的C类的用法和其他C类几乎一样。没什么特别的。
重要:使用导出C类的DLL和使用静态库没有区别。所有适用于用C代码编译的静态库的规则都完全适用于导出C类的dll.
所见即所得的
细心的读者一定注意到了Dependency Walker工具显示了一个额外的导出成员,即CXyz CXyz:operator=(const CXyz)赋值操作符。你在工作中看到的是C的收入(翻译:我猜这是原作者幽默的说法,意思是你没有定义an=赋值运算符,而是编译器自动给你定义了一个,而不是收入是多少?)。根据C标准,每个类都有四个指定的成员函数:
默认构造函数
复制构造函数
赋值运算符(运算符=)
如果类的作者没有声明和提供这些成员的实现,C编译器将声明它们并产生一个隐式的默认实现。在CXyz类中,编译器认定其默认构造函数、复制构造函数和析构函数无意义,优化后将其消除。赋值运算符在优化后仍然有效,并从DLL中导出。
要点:使用__declspec(dllexport)指定类导出,告诉编译器尝试导出与该类相关的任何内容。它包括所有类的数据成员、所有类的成员函数(要么显式声明,要么由编译器隐式生成)、所有类的基类及其所有成员。考虑:
//MSVisualC编译器将发出c 4275警告,因为基类未导出。
class__declspec(dllexport)派生自:
公共数据库
.
私人:
Datam _ data//c 4251警告,因为没有导出任何数据成员。
在上面的代码片段中,编译器会警告您基类和类的数据成员没有导出。因此,为了成功导出一个类,开发人员需要导出所有相关的基类和所有类的已定义数据成员。这种滚雪球式的出口要求是一个主要缺点。这就是为什么比如导出STL派生的模板类或者使用STL模板类对象作为数据成员是非常困难和烦人的。例如,STL容器(如std:map实例)可能需要导出许多额外的内部类。
异常安全
导出的C类可能会抛出异常而没有任何错误。因为DLL及其客户端使用同一类型编译器的同一版本,所以C异常将被捕获并被抛出到DLL的范围之外,就好像DLL没有分界线一样。记住,使用带有导出C的代码与使用带有相同代码的静态库是完全一样的。
优势
导出的C类的使用方式和其他C类一样。
l客户端可以毫不费力地捕捉DLL中的异常。
当DLL模块中有一些小的代码更改时,其他模块不需要重新生成。这对于具有许多复杂和困难代码的大型项目非常有用。
按照业务逻辑在一个大项目中实现DLL,可以算是真正模块划分的第一步。总的来说,把项目模块化是值得做的。
劣势
从DLL导出的c类需要与它的对象和用户保持密切联系。DLL应该被看作是一个静态库,考虑到了代码依赖性。
l客户端代码和DLL必须与同一版本的CRT动态连接。为了纠正模块间CRT资源的记录,这一步是必要的。如果客户端和DLL连接到不同版本的CRT或静态连接到CRT,则在一个CRT实例中应用的资源可能会在另一个CRT实例中释放。它会破坏CRT实例的内部状态,试图操作外部资源,很可能导致运行失败。
l客户端代码和DLL必须就异常处理和生成达成一致,编译器中的异常设置也必须一致。
导出C类需要同时导出这个类所有相关的东西,包括它的所有基类,所有类定义使用的数据成员等等。
成熟的方法:使用抽象接口
一个C抽象接口(比如一个纯虚函数、没有数据成员的C类)试图做到两全其美:一个独立于对象的编译器规则的接口和一个方便的面向对象的函数调用。为了满足这些需求,我们需要提供一个接口声明的头文件,并实现一个可以返回新创建的对象实例的工厂函数。只有此工厂函数需要使用__declspec(dllexport/dllimport)指定。该接口不需要任何额外的规范。
virtualintFoo(intn)=0;
virtualvoidRelease()=0;
//创建Xyz对象实例的工厂函数
extern c xyzapiixyz * APIENTRYGetXyz();
在上面的代码片段中,工厂函数GetXyz被声明为extern XYZAPI。这样做是为了防止函数名被修饰。这样,这个函数在外部表现为一个常规的C函数,它很容易被兼容C的编译器识别。也就是说,当使用抽象接口时,客户端代码如下所示:
C不需要为其他编程语言(如C#或Java)使用的接口提供特定的标签。但这并不意味着C不能声明和实现接口。设计C接口的一般方法是声明一个没有任何数据成员的抽象类。这样,派生类可以继承和实现这个接口,但是这个实现对客户端是不可见的。接口的客户端不需要知道和关注接口是如何实现的。它只需要知道函数是可用的,以及它们是做什么的。
内部机制
这种方法背后的想法非常简单。一个纯虚函数组成的类,成员很少,只是虚函数表——和函数指针数组。在DLL的范围内,这个函数指针数组由它的作者用他认为必要的东西填充。这样,DLL外部使用的指针数组就是调用接口的实际实现。下面是IXyz接口的使用图。
上图演示了IXyz接口被DLL和EXE模块使用。在DLL模块内部,XyzImpl类从IXyz接口派生并实现其方法。EXE的函数调用通过虚拟表的实际实现来引用DLL模块。
为什么这个DLL可以和其他编译器一起运行?
简短的解释是:因为COM技术是和其他编译器一起运行的。现在详细解释一下,其实就是用一个成员很少的虚拟基类作为模块之间的接口。准确的说,COM对外公开了一个COM接口。我们知道,虚拟表的概念可以非常准确地添加COM标准的标记。这不是巧合。c语言作为跨越了至少十年的主流开发语言,在COM编程中得到了广泛的应用。因为C天生支持面向对象的特性。微软将其作为工业COM开发的重量级工具也就不足为奇了。作为COM技术的拥有者,微软保证了COM的二进制标准及其在Visual C编译器中实现的C对象模型能够以最低的成本匹配。
难怪其他编译器厂商也采用了和微软一样的方式来实现虚拟表的布局。毕竟大家都希望支持COM技术,兼容微软现有的解决方案。假设一个C编译器不能有效支持COM,注定会被Windows市场抛弃。这就是为什么今天,通过一个抽象接口从一个DLL导出一个C类可以在Windows平台上用一个像样的编译器可靠地工作。
使用智能指针。
为了确保正确的资源释放,虚拟接口提供了一个额外的功能来清除对象实例。手动调用这个函数很枯燥,容易出错。我们都知道这个错误是C世界常见的错误,开发者要记得释放显式函数调用获得的资源。这就是为什么典型的C代码依赖智能指针使用RAII(资源获取就是初始化)的习惯。XyzExecutable项目提供了一个使用AutoClosePtr模板的示例。AutoClosePtr template是最简单的智能指针,它调用销毁类实例的主观方法,而不是delete运算符。下面是一个代码片段,演示了智能指针与IXyz接口的使用:
在任何情况下,使用智能指针将确保Xyz对象得到适当的资源。因为一个错误或者内部异常,函数会过早退出,但是C语言保证在函数退出之前可以调用所有局部对象的析构函数。
异常安全
就像COM接口一样,不再允许因为任何内部异常而泄漏资源,抽象类接口也不会让任何内部异常突破DLL的范围。函数调用将使用返回代码来明确指出错误。对于特定的编译器,C异常的处理是特定的,不能共享。因此,从这个意义上说,抽象类接口的行为与C函数完全一样。
优势:
导出的C类可以被任何C编译器通过抽象接口使用。
DLL的c运行时和DLL的客户端是相互独立的。因为资源的初始化和释放都发生在DLL内部,所以客户端不受DLL的C运行时选择的影响。
l真正的模块分离可以高度完美的实现。结果模块可以重新设计和重新生成,而不受项目其余模块的影响。
如果需要,DLL模块可以很容易地转换成真正的COM模块。
缺点:
一个显式函数调用需要创建一个新的对象实例并删除它。虽然一个智能指针可以避免开发者之后的调用。
l抽象接口函数不能返回或接受常规C对象作为参数。它只能接受内置类型(如int、double、char*等。)或另一个虚拟接口作为参数类型。它具有与COM接口相同的限制。
STL模板类是如何工作的?
c标准模板库容器(如vector、list或map)等模板并没有设计成DLL模块(带有抽象类接口)。没有关于DLL的C标准,因为DLL是一种特定于平台的技术。C标准不需要出现在其他不使用C语言的平台上。目前微软的Visual C编译器可以导出和导入开发者用__declspec(dllexport/dllimport)关键字显式标识的STL类实例。编译器会发出几个恼人的警告,但它仍然可以运行。但是,您必须记住,导出STL模板实例与导出规则类C完全相同,具有相同的限制。所以,STL在这方面没什么特别的。
摘要
本文讨论了从DLL模块导出C对象的几种不同方法。详细讨论了每种方法的优缺点。以下是一些结论:
l导出一个具有完整C函数的对象,具有最广泛的开发环境和开发语言兼容性。然而,为了使用现代编程范例,DLL用户需要使用过时的C技能来为C接口制作额外的封装层。
l导出一个常规的C类和用C代码提供一个单独的静态库没有区别。用法非常简单和熟悉,但DLL与客户端有着非常密切的联系。DLL及其客户端必须使用相同版本和相同类型的编译器。
l定义一个没有数据成员的抽象类并在DLL内部实现它是导出C对象的最好方法。到目前为止,这种方法在DLL和它的客户端之间提供了一个清晰的、定义良好的面向对象的接口。这样的DLL可以被Windows平台上的任何现代C编译器使用。接口智能指针的使用几乎和导出的C类一样方便。
批准
本文,包括任何源代码和文件,都遵循Code Project开放许可(CPOL)协议。
作者简介
亚历克斯布雷克曼职业:软件开发员
国籍:以色列
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。