简介:
动态库(DLL)从开始就作为windows平台的组成部分而存在。它以独立的模块把c函数封装起来供其他用户使用 。DLL从开始就是以封装C语言的形式而存在,当然现在你也可以封装其他语言,比如c++,而如果要实现跨平台使用DLL,则我们必须回归到C语言。
利用C语言接口并不意味着我们必须丢弃掉面向对象方法。C语言可以实现应用二进制接口(ABI),这样使调用者和被调用着可以遵从统一的标准,但是C++语言没有这个特性,导致从一个编译器生成的binary不能被另一个编译器所识别。这样使得直接导出C++类就成了冒险。
这篇文章的目的是用不同的方法从DLL模块导出C++ 类。我们假设有一个类Xyz,类里面有一个成员函数Foo。
类Xyz的实现在DLL里,此DLL可以被不同的用户以下列方式所调用:
源代码包含两个工程:
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)
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++异常。如果对象函数需要报告错误,只能通过返回错误码实现。
/* void* GetSomeOtherObject(void) is declared elsewhere. */
XYZHANDLE h = GetSomeOtherObject();
/* Oops! Error: Calling Xyz.Foo on wrong object intance. */
XyzFoo(h, 42);
XyzRelease
。如何忘记调用XyzRelease
则会引起内存泄漏。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);
细心的读者应该注意到,用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个以上的类。
__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模块具体的函数实现。
使用智能指针实现了RAII(资源申请在初始化),这样在不需要的时候会自动释放资源。而不用像在C语言里实现的那样,程序员必须记得在哪里释放资源。
#include "XyzLibrary.h" #include <memory> #include <functional> ... typedef std::shared_ptrIXyzPtr; 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++容器(vector,list,map)不是用来设计DLL的。C++标准对DLL保持沉默,因为DLL是跟平台相关的技术,在其他平台上并不是必须存在的。MS Visual C++可以导出或者导入STL类,只要我们在前面加上__declspec(dllexport/dllimport)
标志。可以工作但是编译器会给出警告信息。我们应该知道,导出STL类与导出一般C++类一样,都会有前面(C++内在方法:导出类)所提及的缺点。
具体见原文