方案一:个人认为算是比较“循规蹈矩”的一种
在DLL中写好接口的实现代码后,然后提供一个申明接口的头文件供调用者使用,我想一般都会这样写:
#ifdef __DLLNAME_XX
#define _XX_LOADDLL extern "C" _declspec(dllexport)
#else //__DLLNAME_XX
#define _XX_LOADDLL extern "C" _declspec(dllimport)
#endif //__DLLNAME_XX
// 主要给调用该DLL的模块使用,应采用自己的命名规则,避免与其它的DLL重名
Const THAR strYourDllName[] = _T("XXX.dll"); // DLL的名称
_XX_LOADDLL bool InterfaceA(LPCTSTR/*[in]*/);
typedef bool (*LP InterfaceA)(LPCTSTR/*[in]*/);
…//其它的接口声明
定义宏__DLLNAME_XX的作用
如果我们在DLL的工程设置中添加了__DLLNAME_XX的定义,这样DLL的工程由于定义了__DLLNAME_XX,则_XX_LOADDLL的值为extern "C" _declspec(dllexport),即接口_XX_LOADDLL bool InterfaceA(LPCTSTR/*[in]*/);将被展开为extern "C" _declspec(dllexport) bool InterfaceA(LPCTSTR/*[in]*/);
它正好表示为一个“导出函数”,而调用该DLL的工程由于没有定义__DLLNAME_XX,则_XX_LOADDLL的值为extern "C" _declspec(dllimport),即在外部该接口将被展开为
extern "C" _declspec(dllimport) bool InterfaceA(LPCTSTR/*[in]*/);
而它表示该接口为一个“导入接口”,这样是不是很巧妙呢?
温馨提示
在接口申明的头文件中最好不要出现如CString等只有在MFC中才会出现的变量类型,尽量用通用的变量类型,例如Cstring用LPCTSTR代替等,这样VB的程序也能调用你的接口,如果用CString的话,VB的程序就识别不了了。
此方案提供给调用者的不是一个接口申明头文件,而是一个类(一般是两个文件,一个.cpp和一个.h),该类中封装了DLL中的所有接口的实现,通过这种方案,调用者就可以像使用一个普通的类一样来使用你的DLL的接口了,是不是觉得很方便呢!呵呵,下面详细介绍这种方案的实现步骤。
名字暂且称为CDll吧,先介绍头文件,头文件一般需要这样写:
Class CDll
{
Public:
// 构造函数
CDll();
// 析构函数
Virtual ~CDll();
Public:
// 如下两个DLL是必须的
BOOL Init(); // DLL的初始化,负责DLL的加载,接口的导入
BOOL UnInit(); // 释放DLL
public
// 接口
Bool InterfaceA(); // 该接口中调用DLL中对应的接口
// 其它接口
Private:
HMOUDLE m_handle; // DLL 的句柄
LP InterfaceA m_pfn InterfaceA; // 假设DLL有这样一个接口
// 其它接口
}
几个需要注意的地方:
Ø CDll中必须要提供DLL中所有接口的调用,而且必须是一对一的方式,不要去改动DLL的调用逻辑,CDll只是提供一个简单接口调用的过渡而已。
Ø CDll中成员函数的名称应尽量和DLL接口函数的名称类似,这样看起来比较统一,可读性好。
写完头文件后,就让我们实现.cpp文件吧:
Ø 构造函数
CDll()::CDll()
{
m_handle = NULL; // 句柄置为空
m_pfn InterfaceA = NULL; // 接口指针初始化为空
// 其它初始化工作
}
Ø 析构函数
CDll()::~CDll()
{
If ( !m_handle )
{
UnInit(); // 释放DLL,防止调用者忘记释放DLL
}
// 其它内存的清理工作
}
Ø BOOL Init () 初始化函数的写法
{
If ( m_handle )
{
Return TRUE;
}
m_handle = ::LoadLibrary(_T(“CDll.dll”)); // 加载DLL
If ( !m_handle )
{
Return FALSE; // DLL加载失败,直接返回
}
m_pfn InterfaceA =( LP InterfaceA)::GetProAddress(m_handle, _T(“接口名”);
// 其它接口按同样的方式进行初始化
Return TRUE;
}
Ø BOOL UnInit () 反初始化函数的写法
{
If ( m_handle )
{
::FreeLibrary(m_handle);
m_pfn InterfaceA = NULL;
// 其它接口指针也置为空
}
Return TRUE;
}
Ø Bool CDll::InterfaceA() // 终于到接口的实现了,呵呵
{
If ( Init() ) // 主要是为使用者提供方便,不用事先调用Init也能直接使用接口
{
If (m_pfnInterfaceA )
{
Return m_pfnInterfaceA();
}
}
Return FALSE;
}
Ø 其它接口的实现,参照CDll::InterfaceA()的实现方法就可以了
到这里,我们的接口类就全部写完了,调用者要使用DLL的接口就非常的方便了,例如要使用DLL的_XX_LOADDLL bool InterfaceA(LPCTSTR/*[in]*/);接口,现在只用这样就可以了:
CDll dll;
dll. InterfaceA();
是不是觉得很方便呢??呵呵…而且这种方式实现的类是一个可以重用的类,充分体现了面向对象中代码重用的思想。
和方案一一样,需要提供一个头文件,不管DLL有多少个接口,对外开放的接口却只有一个,但是调用者可以访问DLL的所有接口,是不是觉得很玄乎?呵呵,不卖关子了,直接进入主题。头文件一般需要像这样写:
typedef bool (*LPInterfaceA)(LPCTSTR/*[in]*/); // 不用extern “C”…了
// 其它接口的申明
注意这些接口必须在DLL代码中实现。
typedef struct INTERFACE
{
LPInterfaceA m_pfnInterfaceA; // 接口地址
// 其它接口的定义
Void Init()
{
m_pfnInterfaceA = NULL;
// 其它接口也要初始化为NULL
}
}INTERFACE,*LPINTERFACE;
_XX_LOADDLL bool LPGetAllInterface(INTERFACE* pInterface);
typedef bool (*LPGetAllInterface)( INTERFACE* pInterface /*[in/out]*/);
接口LPGetAllInterface必须完成pInterface各成员的初始化工作,比较推荐的方式是:在DLL内部将LPInterfaceA (LPCTSTR/*[in]*/)定义为全局变量,而LPGetAllInterface接口就可以这样实现
LPGetAllInterface( INTERFACE* pInterface /*[in/out]*/)
{
m_pfnInterfaceA = LPInterfaceA; // 函数名就是代表函数起始地址
// 其它接口的初始化
}
至此,整个头文件就写完了,调用者就可以这样调用DLL了,
INTERFACE interface;
interface.Init();
LPGetAllInterface lpGetAllIterface;
lpGetAllInterface(&interface);
if ( interface. m_pfnInterfaceA)
{
interface.m_pfnInterfaceA(lpszText); // 调用LPInterfaceA接口
}
此方案是受到COM编程思想的启发,这样一来,对外界来说,DLL永远只有一个不变的接口,在某些场合,这是非常重要的,而且这种方法可以在不改变接口的情况下增加新的接口,新的功能,虽然这句话听起来有点拗口,但却是一种很好扩展DLL功能的实现方案。
第一种方案是一种比较传统的方式,而第二种方式充分体现了面向对象的编程思想,实现了代码的重用,而第三种方式个人持“中立”观点,一般不会用到它,我之所以把它也写出来,是希望我们的开发思路不要一直走直线,偶尔转转弯,也许你真的能发现一条捷径!