工作中遇到需要从DLL中导出c++类的问题,在C语言中,在调用者和被调用者之间的二进制接口被很好的定义并被广泛接受,但在C++中里却没有可识别的应用程序二进制接口。实际上,由一个C++编译器产生的二进制代码并不能被其它C++编译器兼容。再者,在同一个编译器但不同版本的二进制代码也是互不兼容的。所有这些导致从一个DLL中一个C++类简直就是一个冒险。针对这一问题,查阅了相关资料和前人博客,对如何较好的导出c++类进行了记录,并对示例代码做了部分补充。
附上参考连接:
1、HowTo: Export C++ classes from a DLL
2、https://blog.csdn.net/clever101/article/details/3034743
文中的示例代码包含两个工程:
XyzLibrary —— 一个DLL工程;
XyzExecutable —— 一个Win32 使用"XyzLibrary.dll"的控制台程序;
一般在生成dll的头文件中添加如下代码,这样生成dll程序和调用dll程序可以使用同一个头文件。
#if defined(XYZLIBRARY_EXPORT) // inside DLL
#define XYZAPI __declspec(dllexport)
#else // outside DLL
#define XYZAPI __declspec(dllimport)
#endif // XYZLIBRARY_EXPORT
使用一个HWND句柄来代表一个窗口。虚构的Xyz对象通过下面这样一种方式导出一个C接口
typedef tagXYZHANDLE {} * XYZHANDLE;
// Factory function that creates instances of the Xyz object.
XYZAPI XYZHANDLE APIENTRY GetXyz(VOID);
// Calls Xyz.Foo method.
XYZAPI INT APIENTRY XyzFoo(XYZHANDLE handle, INT n);
// Releases Xyz instance and frees resources.
XYZAPI VOID APIENTRY XyzRelease(XYZHANDLE handle);
// APIENTRY is defined as __stdcall in WinDef.h header.
客户端调用代码:
#include "XyzLibrary.h"
/* Create Xyz instance. */
XYZHANDLE hXyz = GetXyz();
if(hXyz)
{
/* Call Xyz.Foo method. */
XyzFoo(hXyz, 42);
/* Destroy Xyz instance and release acquired resources. */
XyzRelease(hXyz);
/* Be defensive. */
hXyz = NULL;
}
注意点:
1、使用这种方式,一个DLL必须提供显式的对象构建和删除函数;
2、统一调用协定,需要客户端的调用协定和DLL的调用协定匹配。 这里使用了APIENTRY宏,这个宏在"WinDef.h"这个头文件里被定义为__stdcall;
__stdcall是一种函数调用协议 ,其他的还有__cdecl和__fastcall函数调用协议,函数调用协议会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。
(1)函数参数由右向左入栈;
(2)函数调用结束后由被调用函数清除栈内数据;
(3)在C语言编译器,函数名被修饰为“_functionname@number”(“functionname”为函数名,“number”为参数字节数);
(4)在C++语言编译器中,函数名被修饰为“?functionname@@YG******@Z”(“******”为函数返回值类型和参数类型表);
导出一个类和导出一个C函数非常相似。在类名之前使用__declspec(dllexport/dllimport)关键字来指定假如整个类都需要被导出,或者在指定的函数声明前指定假如只是特定的类函数需要被导出。
XyzLibrary.h代码:
//导出基类;
class XYZAPI Base{
...
};
class XYZAPI Data{
...
};
// 整个CXyz类被导出,包括它的函数和成员
class XYZAPI CXyz:public Base{
public:
int Foo(int n);
};
// 只有 CXyz::Foo函数被导出
//
class CXyz{
public:
XYZAPI int Foo(int n);
private:
Data m_data;
};
客户端代码:
#include "XyzLibrary.h"
...
// 客户端使用Xyz对象作为一个规则C++类.
CXyz xyz;
xyz.Foo(42);
缺点:
1、为了成功导出一个c++类,需要求导出该类所有相关基类、所有类定义用到的数据成员等。 这个滚雪球般的导出要求是一个重大缺点。这也是为什么,比如,导出派生自STL模板类或者使用STL模板类对象作为数据成员是非常困难和令人生厌的。比如一个STL容器比如std::map<>实例可能要求导出数十个额外的内部类;
2、 客户端代码和DLL必须在异常处理和产生达成一致,同时在编译器的异常设置也必须一致;
3、高耦合,从一个DLL中导出C++类在它的对象和使用者需要保持紧密的联系。 DLL应该被视作一个带有考虑到代码依赖的静态库;
优点:
1、一个导出的C++类和其它任何C++类的用法是一样的;
2、客户端能毫不费力地捕捉在DLL发生的异常;
3、当在一个DLL模块内有一些小的代码改动时,其它模块也不用重新生成。这对于有着许多复杂难懂代码的大工程是非常有用的;
4、在一个大工程中按照业务逻辑分成不同的DLL实现可以被认为真正的模块划分的第一步。总的来说,它是使工程达到模块化值得去做的事;
一个C++抽象接口(比如一个拥有纯虚函数和没有数据成员的C++类)可以做到两全其美:对对象而言,独立于编译器的规则的接口以及方便的面向对象方式的函数调用。为达到这些要求就是提供一个接口声明的头文件,同时实现一个能返回最新创建的对象实例的工厂函数。只有这个工厂函数需要使用__declspec( dllexport/dllimport )指定。接口不需要任何额外的指定;
XyzLibrary.h代码:
// Xyz object的抽象接口,被客户端和dll共同使用;
// 不要求作额外的指定
class IXyz{
virtual int Foo(int n) = 0;
virtual void Release() = 0;
};
// 创建Xyz对象实例的工厂函数
extern "C" XYZAPI IXyz* APIENTRY GetXyz();
extern "C" XYZAPI void APIENTRY ReleaseObj(IXyz* pIXyz);
XyzLibrary.cpp代码:
#include "XyzLibrary.h"
#include "XyzImpl.h" //派生类的声明;
XYZAPI IXyz* APIENTRY GetXyz(){
return new IXyzImpl;//new一个派生类实例,用于实现功能;
}
//这里不能直接delete pIXyz,因为没有把IXyz的析构函数定义为虚函数;
XYZAPI void APIENTRY ReleaseObj(IXyz* pIXyz){
pIXyz->Release();
}
XyzImpl.h代码:
//派生类头文件,负责对纯虚函数进行具体功能的重定义;
#include "XyzLibrary.h"
class IXyzImpl : public IXyz
{
public:
virtual int Foo(int n);
virtual void Release();
~IXyzImpl();
private:
};
客户端代码:
#include "XyzLibrary.h"
...
IXyz* pXyz = GetXyz();
if(pXyz){
pXyz->Foo(42);
ReleaseObj(pIXyz);
pXyz = NULL;
}
优点:
1、 一个导出的C++类能够通过一个抽象接口,被用于任何C++编译器;
2、一个DLL的C运行库和DLL的客户端是互相独立的。 因为资源的初始化和释放都完全发生在DLL内部,所以客户端不受DLL的C运行库选择的影响。
**3、真正的模块分离能高度完美实现。**结果模块可以重新设计和重新生成而不受工程的剩余模块的影响。如果需要,一个DLL模块能很方便地转化为真正的COM模块。
缺点:
1、 一个显式的函数调用需要创建一个新的对象实例并删除它。 尽管一个智能指针能免去开发者之后的调用;
2、 一个抽象接口函数不能返回或者接受一个规则的C++对象作为一个参数。 它只能以内置类型(如int、double、char*等)或者另一个虚接口作为参数类型。它和COM接口有着相同的限制;
关于内存释放的问题:
为了确保正确的资源释放,一个虚接口提供了一个额外的函数来清除对象实例,但手动调用这个函数令人厌烦并容易导致错误发生,所以可以用智能指针方式来管理资源释放。
这种方法背后的思想是,因为一个由纯虚函数组成的成员很少的类只不过是一个虚函数表——一个函数指针数组。在DLL范围内这个函数指针数组可以填充任何必需的东西。这样这个指针数组在DLL外部使用就是调用接口的派生类上的实现。下面是IXyz接口的用法说明图表;
上面的图表示了IXyz接口被DLL和EXE模块二者都用到。在DLL模块内部,XyzImpl类派生自IXyz接口并实现它的方法。在EXE的函数调用引用DLL模块经过一个虚表的实际实现;
这篇文章讨论了几种从一个DLL模块中导出一个C++对象的不同方法。对每种方法的优点和缺点的详细论述也已给出。下面是得出的几个结论:
1、 以一个完全的C函数导出一个对象有着最广泛的开发环境和开发语言的兼容性。然而,为了使用现代编程范式一个DLL使用者被要求使用过时的C技巧对C接口作一层额外的封装。
2、导出一个规则的C++类和以C++代码提供一个单独的静态库没什么区别。用法非常简单和熟悉,然而DLL和客户端有着非常紧密的连接。DLL和它的客户端必须使用相同版本和相同类型的编译器。
3、定义一个无数据成员的抽象类并在DLL内部实现是导出C++对象的最好方法。到目前为止,这种方法在DLL和它的客户端提供了一个清晰的,明确界定的面向对象接口。这样一种DLL能在Windows平台上被任何现代C++编译器所使用。接口和智能指针一起结合使用的用法几乎和一个导出的C++类的用法一样方便。
有错误之处请多多指教哈!!!