DLL允许用外部用户可以使用的显式C函数列表将一部分功能封装在独立模块中。1980年,当Windows DLL被推广到世界时,对广大开发人员来说唯一可行的选择就是C语言。因此,Windows dll自然将其功能公开为C函数和数据。在内部,一个DLL可以用任何语言实现,但是为了在其他语言和环境中使用,一个DLL接口应该回到最小的公分母——C语言。
使用C接口并不意味着开发人员应该放弃面向对象的方法。即使是C接口也可以用于真正的面向对象编程,但是它可能是一种冗长乏味的方式。C++与C语言相反,在C语言中,调用者和被调用者之间的二进制接口定义良好并被广泛接受,在c++世界中,没有可识别的应用程序二进制接口(ABI)。实际上,这意味着c++编译器生成的二进制代码与其他c++编译器不兼容。此外,同一个c++编译器的二进制代码可能与该编译器的其他版本不兼容。所有这些使得从DLL导出c++类变得非常困难。
本文的目的是展示几种从DLL模块导出c++类的方法。Xyz对象的实现在DLL中,可以分发给广泛的客户端。用户可以通过以下方式访问Xyz功能:
源代码包含两个工程:
面向对象编程的经典C语言方法是使用不透明指针,即handle。用户调用在内部创建对象的函数,并返回该对象的句柄。然后,用户调用接受句柄作为参数的各种函数,并对对象执行各种操作。句柄用法的一个很好的例子是Win32窗口API,它使用HWND句柄来表示窗口。假想的Xyz对象通过C接口导出,像这样:
typedef struct tagXYZHANDLE{}*XYZHANDLE;
// void pointer point to c++ object
EXTERN_C XYZAPI XYZHANDLE GetXyz();
// initialize a object
EXTERN_C XYZAPI int XyzFoo(XYZHANDLE handle, int n);
// cpp class function API for user
EXTERN_C XYZAPI void XyzRelease(XYZHANDLE handle);
// release handle(obejct)
所有关于的类定义全部隐藏在cpp中,头文件中只有handle定义以及对外接口的定义。至于这种接口设计怎么使用,可以参考下面的代码:
#include "Xyz.h"
#include
int main()
{
XYZHANDLE handle = GetXyz(); // create a object
int k = XyzFoo(handle, 4); // use class interface
std::cout << k << std::endl;
XyzRelease(handle); // release memory
return 0;
}
这种方法需要显式的调用创建和释放对象的函数。
不允许c++异常跨越DLL边界。C语言对c++异常一无所知,也不能正确地处理它们。如果一个对象方法需要报告一个错误,那么应该使用一个返回值表示错误。
几乎存在于Windows平台上的所有现代c++编译器都支持从DLL导出c++类。导出一个c++类非常类似于导出C函数。如果需要导出整个类,开发人员需要做的就是在类名之前使用declspec(dllexport/dllimport)说明符;如果只需要导出特定的类方法,则在方法声明之前使用。以下是代码片段:
class XYZAPI CXyz
{
public:
int foo(int n);
} // whole class is export with all methods and members
class CXyz
{
public:
XYZAPI int foo(int n);
} // only foo method is export.
根据c++标准,每个类都有四个特殊的成员函数:
如果类的作者没有声明也没有提供这些成员的实现,那么c++编译器将声明它们,并生成隐式的默认实现。对于CXyz类,编译器认为默认构造函数、复制构造函数和析构函数都很简单,并对它们进行了优化。但是,赋值操作符经过了优化并从DLL中导出。
还有一个非常重要的问题,如下所示:
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.
};
在上面的代码片段中,编译器将警告您没有导出数据成员的基类和类。因此,为了成功导出一个c++类,开发人员需要导出所有相关的基类和用于定义数据成员的所有类。这种滚雪球式导出要求是一个显著的缺点。这就是为什么导出从STL模板派生的类或者使用STL模板作为数据成员是非常困难和无聊的。
导出的c++类可能会毫无问题地抛出异常。因为一个DLL和它的客户端都使用相同版本的同一个c++编译器,所以c++异常会跨DLL边界抛出和捕获,就好像根本没有边界一样。请记住,使用导出c++代码的DLL与使用具有相同代码的静态库是相同的。
一个c++抽象接口(例如一个只包含纯虚函数而不包含数据成员的c++类)试图两全其美:一个独立于编译器的对象接口,以及一个方便的面向对象的方法调用方式。所需要做的就是提供带有接口声明的头文件,并实现返回新创建对象实例的工厂函数。只有工厂函数必须用__declspec(dllexport/dllimport)说明符声明。该接口不需要任何额外的说明符。
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”。这是必需的,以防止混淆函数名。因此,这个函数被公开为一个普通的C函数,并且很容易被任何兼容C的编译器识别。当使用抽象接口时,客户端代码看起来是这样的:
#include "Xyz.h"
#include
int main()
{
IXyz *xyz = GetXyz();
int k=xyz->Foo(12);
xyz->Release();
std::cout << k << std::endl;
return 0;
}
c++不像其他编程语言(例如c#或Java)那样为接口提供特殊的概念。但这并不意味着c++不能声明和实现接口。创建c++接口的常见方法是声明一个没有任何数据成员的抽象类。然后,另一个单独的类继承接口并实现接口方法,这样可以实现对接口客户端隐藏实现。客户端既不知道也不关心接口是如何实现的。它只知道哪些方法是可用的,以及它们的作用。
MS Visual c++的最新版本提供了带有标准c++库的智能指针。下面是使用std::shared_ptr类的Xyz对象的例子:
#include "Xyz.h"
#include
#include
#include
int main()
{
typedef std::shared_ptr IXyzPtr;
IXyzPtr ptr(GetXyz(), std::mem_fn(&IXyz::Release));
if (ptr)
{
int k = ptr->Foo(12);
std::cout << k << std::endl;
}
return 0;
}
就像COM接口不允许泄漏任何内部异常一样,抽象c++接口也不允许任何内部异常突破DLL的边界。类方法应该使用返回代码来指示错误。处理c++异常的实现对每个编译器都是特定的,不能共享。因此,在这方面,一个抽象的c++接口应该表现得像一个普通的C函数。
原文链接:HowTo: Export C++ classes from a DLL
本文示例代码:github