首页 > 代码库 > 从dll中导出c++类
从dll中导出c++类
简介:
动态库(DLL)从开始就作为windows平台的组成部分而存在。它以独立的模块把c函数封装起来供其他用户使用 。DLL从开始就是以封装C语言的形式而存在,当然现在你也可以封装其他语言,比如c++,而如果要实现跨平台使用DLL,则我们必须回归到C语言。
利用C语言接口并不意味着我们必须丢弃掉面向对象方法。C语言可以实现应用二进制接口(ABI),这样使调用者和被调用着可以遵从统一的标准,但是C++语言没有这个特性,导致从一个编译器生成的binary不能被另一个编译器所识别。这样使得直接导出C++类就成了冒险。
这篇文章的目的是用不同的方法从DLL模块导出C++ 类。我们假设有一个类Xyz,类里面有一个成员函数Foo。
类Xyz的实现在DLL里,此DLL可以被不同的用户以下列方式所调用:
- 纯C
- 一般C++
- c++抽象接口
源代码包含两个工程:
- XyzLibrary – DLL库
- XyzExecutable – Window 32 应用程序
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在工程XyzLibrary代表__declspec(dllexport),而在客户工程(XyzExecutable)代表__declspec(dllimport)
C语言方法
句柄
传统C语言可以利用指针或者句柄实现面向对象语言的一些特性,比如调用一个函数来创建一个类对象,并用次对象作为参数来调用对象的不同函数操作。比如,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;
}
此方法中,DLL必须提供创建对象和清理对象的函数接口。调用规则
对于所有的输出函数必须定义调用规则,忽略调用规则对于初学者来说是非常常见的错误。如果用户调用规则与DLL相一致,则一切OK,反之则会在运行时出错。XyzLibrary工程利用APIENTRY
宏,这个宏在文件"WinDef.h"被定义为__stdcall
。
异常安全
C++异常不能从DLL中抛出,C语言不知道也不能处理C++异常。如果对象函数需要报告错误,只能通过返回错误码实现。
优点
- DLL可以被不广泛的调用。几乎所有的现代语言都支持与纯C函数的交互。
- DLL和用户的C实时库互相彼此独立。因为资源申请和释放完全在DLL模块,用户不受DLL 所选择的CRT的影响。
缺点
- 因为对象和函数分离,使得调用对象和函数完全依赖于用户。以下面的代码为例,编译器不能发现错误。
/* void* GetSomeOtherObject(void) is declared elsewhere. */ XYZHANDLE h = GetSomeOtherObject(); /* Oops! Error: Calling Xyz.Foo on wrong object intance. */ XyzFoo(h, 42);
- 用户必须显式地调用函数创建和删除对象实例。用户必须在所有的推出函数中调用
XyzRelease
。如何忘记调用XyzRelease
则会引起内存泄漏。
C++内在方法:导出类
Windows平台上几乎所有的C++编译器都支持从DLL导出类。导出类与导出函数一样:如果导出整个类的话,只需要在类前面加上标志__declspec(dllexport/dllimport)
,如果导出类里面特定的成员函数,就在成员函数前面加上标志__declspec(dllexport/dllimport)
。下面是实例代码:
// The whole CXyz class is exported with all its methods and members.
//
class XYZAPI CXyz
{
public:
int Foo(int n);
};
// Only CXyz::Foo method is exported.
//
class CXyz
{
public:
XYZAPI int Foo(int n);
};
没有必要显式的为类或者成员函数定义调用规则。C++编译器默认用__thiscall
作为成员函数的调用规则。但是,不同的编译器对于函数的名称修饰方式不同,因此DLL和用户最好用相同版本的编译器。下图是一个visual c++编译器中的函数修饰名称。再次强调,只有MS C++能用这个DLL。为了使调用者和被调用者之间的名称修饰相同,DLL和用户还必须 用同一个版本的MS C++。下面是用户使用Xyz对象的代码。
#include "XyzLibrary.h"
...
// Client uses Xyz object as a regular C++ class.
CXyz xyz;
xyz.Foo(42);
重要:利用导出类的DLL和静态库没有什么区别。所有适用于静态库的规则也适用于动态库(DLL)。
所见非所得
细心的读者应该注意到,用dependency walker会解析出另外一个赋值函数CXyz& CXyz::operator =(const CXyz&)
。根据C++标准,每个类都有特殊的4个函数:
- 默认构造函数
- 拷贝构造函数
- 析构函数
- 赋值函数
如果开发着不定义这些函数,则编译器会自动为我们生成这些函数。对于类CXyz,编译器判断默认构造函数,拷贝构造函数和析构函数无价值,所以把他们优化掉了。而对于赋值函数则导出。
重要:导出一个类,则意味着导出与类相关的一切:类成员变量,类成员函数(无论是显式声明还是隐式生成的),基类。
class Base
{
...
};
class Data
{
...
};
// MS Visual C++ compiler emits C4275 warning about not exported base class.
class __declspec(dllexport) Derived :
public Base
{
...
private:
Data m_data; // C4251 warning about not exported data member.
};
在上面的代码中,编译器警告没有导出基类和类成员变量。所以如果要成功的导出一个类,我们必须导出所有的基类和定义成员变量的类。这个滚雪球式的要求是一个明显的缺点。这也是为什么导出一个继承自STL模版或者含有STL模版成员函数的类是多么痛苦的一件事。导出一个STL map实例至少需要导出相关的10个以上的类。异常安全
导出的C++类可以抛出异常。因为DLL和用户都用相同版本的编译器,C++异常可以被抛出或者捕获。优点
- 导出的C++类可以像其他C++类一样使用
- 在DLL模块内部抛出的异常可以在用户端捕获
- DLL内部代码的小改动,不需要重新编译其他模块。这在大的工程里面特别有用。
- 把一个大工程的不同逻辑模块生成不同的DLL被当作是通向模块化的第一步
缺点
- 从DLL导出类并不能阻止对象和用户之间的耦合。在函数依赖方面,DLL被看作静态库。
- 用户代码和DLL必须用同一个CRT动态链接。如果用户代码和DLL链接的CRT版本不一样,或者静态链接到CRT,那么在一个CRT中申请的资源将在不同的CRT实例中释放。这将破坏CRT的内部状态,试图操作外部资源很可能导致程序崩溃。
- 用户代码和DLL必须遵从相同的异常处理模型,相同编译器异常设置。
- 导出C++类需要导出与此类相关的一切:基类,定义成员函数的类等。
C++成熟方法:利用纯虚接口
C++纯虚接口是只只包含纯虚函数没有成员的类。它试图获得最佳的两个方面:独立于编译器的对象接口和方便的面向对象的函数调用。所有的一切只需要声明一个包含接口声明的头文件和实现一个返回一个对象的工厂函数。这个工厂函数需要一个标识符__declspec(dllexport/dllimport)
。实例如下:// The abstract interface for Xyz object.
// No extra specifiers required.
struct IXyz
{
virtual int Foo(int n) = 0;
virtual void Release() = 0;
};
// Factory function that creates instances of the Xyz object.
extern "C" XYZAPI IXyz* APIENTRY GetXyz();
工厂函数GetXyz
被声明为extern "C" 是为了防止函数名字捆绑(name mangling),这样可以被任何C编译器所识别,下面的是客户段代码,说明如何使用这个接口。#include "XyzLibrary.h"
...
IXyz* pXyz = ::GetXyz();
if(pXyz)
{
pXyz->Foo(42);
pXyz->Release();
pXyz = NULL;
}
定义一个不包含任何成员变量纯虚类,然后定义子类并实现接口成员函数。这样用户无需知道这个函数是如何实现的,只需知道它做了什么。如何工作
其实思想很简单:纯虚类只包含了一个虚函数表(包含了多个函数指针的数组)。下图显示了DLL和用户调用的内部实现:
上图显示了纯虚类IXyz作为接口被DLL和用户EXE所使用。在DLL模块内部,类XyzImpl
继承自接口IXyz并实现了方法,EXE模块函数的调用通过虚函数表可以触发DLL模块具体的函数实现。
使用标准C++智能指针
使用智能指针实现了RAII(资源申请在初始化),这样在不需要的时候会自动释放资源。而不用像在C语言里实现的那样,程序员必须记得在哪里释放资源。
#include "XyzLibrary.h"
#include <memory>
#include <functional>
...
typedef std::shared_ptr<IXyz> IXyzPtr;
IXyzPtr ptrXyz(::GetXyz(), std::mem_fn(&IXyz::Release));
if(ptrXyz)
{
ptrXyz->Foo(42);
}
// No need to call ptrXyz->Release(). std::shared_ptr class
// will call this method automatically in its destructor.
异常安全
纯C++接口不能让DLL内部的异常抛出DLL外。类成员函数需要用错误码返回错误。不同的编译器对C++异常处理的实现也会不同,所以不能共享。从这个角度上讲,纯虚C++接口与纯C函数表现的相同。
优点
- 一个C++类能通过虚接口被不同的C++编译器使用
- DLL和用户的CRT(C运行库)互相独立。因为资源的申请和释放完全在DLL模块内部实现,用户不受DLL的CRT所影响。
- 真正的模块分离。DLL模块可以被重新设计和重构而不影响项目的其他模块。
缺点
- 创造和删除对象虚显式函数调用来实现,虽然后者可以通过智能指针而避免。
- 虚接口不同返回或者接受一个普通的C++对象作为参数。它必须是内建类型(int,double,char*等)或者其他的虚接口
关于STL模版类
标准C++容器(vector,list,map)不是用来设计DLL的。C++标准对DLL保持沉默,因为DLL是跟平台相关的技术,在其他平台上并不是必须存在的。MS Visual C++可以导出或者导入STL类,只要我们在前面加上__declspec(dllexport/dllimport)
标志。可以工作但是编译器会给出警告信息。我们应该知道,导出STL类与导出一般C++类一样,都会有前面(C++内在方法:导出类)所提及的缺点。
具体见原文