C++学习笔记 1 :如何从DLL中导出一个c++类

文章目录

  • 前言
    • 方法1:C Language Approach(C语言方式)
      • __stdcall:是Windows API默认的函数调用协议
    • 方法2:Naive Approach(导出c++类)
    • 方法3:Mature Approach(用抽象c++接口)
    • 内部机制
    • 总结

前言

工作中遇到需要从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

方法1:C Language Approach(C语言方式)

使用一个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函数调用协议,函数调用协议会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。

__stdcall:是Windows API默认的函数调用协议

(1)函数参数由右向左入栈;
(2)函数调用结束后由被调用函数清除栈内数据;
(3)在C语言编译器,函数名被修饰为“_functionname@number”(“functionname”为函数名,“number”为参数字节数);
(4)在C++语言编译器中,函数名被修饰为“?functionname@@YG******@Z”(“******”为函数返回值类型和参数类型表);

方法2:Naive Approach(导出c++类)

导出一个类和导出一个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实现可以被认为真正的模块划分的第一步。总的来说,它是使工程达到模块化值得去做的事;

方法3:Mature Approach(用抽象c++接口)

一个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接口的用法说明图表;
C++学习笔记 1 :如何从DLL中导出一个c++类_第1张图片
上面的图表示了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++类的用法一样方便。

有错误之处请多多指教哈!!!

你可能感兴趣的:(Effective,C++,c++)