本文將介紹三種提供DLL接口的方式(對於如何編寫DLL內部的代碼不做介紹),由於筆者工作時間不長,難免有疏漏的之處,還請各位大俠不吝賜教,謝謝!
方案一:個人認爲算是比較“循規蹈矩”的一種
在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的模塊使用,應採用自己的命名規則,避免與其它的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的接口了,是不是覺得很方便呢!呵呵,下面詳細介紹這種方案的實現步驟。
第一步:定義自己的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功能的實現方案。
總結
第一種方案是一種比較傳統的方式,而第二種方式充分體現了面向對象的編程思想,實現了代碼的重用,而第三種方式個人持“中立”觀點,一般不會用到它,我之所以把它也寫出來,是希望我們的開發思路不要一直走直線,偶爾轉轉彎,也許你真的能發現一條捷徑!