動態連結庫(DLL)的建立和使用

Liwuqingxin發表於2018-04-22

最近想做個記錄日誌的C++庫,方便後續使用。想著使用動態庫,正好沒用過,學習下。概念這裡不贅述。學習過程中碰到的幾點,記錄下來。學習是個漸進的過程,本文也是一個逐漸完善的過程。

一、Static Library

標準Turbo 2.0中的C函式庫(scanf、pringf、memcpy等)來自靜態庫。建立方法很簡單,建立win32 application工程,選擇static library,新增變數、方法和類等就可以了。使用的方法如下:

#include "../LogBuilderSL/LogBuilder.h"
#pragma comment(lib, "../Debug/LogBuilderSL.lib")

之後便可以像C庫函式一樣正常使用了。

#pragma comment(lib, "../Debug/LogBuilderSL.lib")表明,本工程與靜態庫(引數指定的路徑下的*.lib)一起編譯。

或者將該lib新增到【Project Property】-->【Linker】-->【Input】下的Additional Dependencies中,新增的方式為全路徑,如:“C:\Users\ SAMSUNG-PC\ Desktop\ C S\ LogBuilderSL\ Debug\ LogBuilderSL.lib”。

再或者將*.lib放置到Library Directories下(或者在其中新增*.lib路徑),在上面的【Input】中新增LogBuilderSL.lib。

二、Dynamic Link Library

對於DLL,VC支援的有三類:No-MFC DLL、MFC Regular DLL和MFC Extension DLL。

  • No-MFC DLL:匯出函式為標準的C介面(extern "C");
  • MFC Regular DLL:包含一個繼承自CWinApp的類,無訊息迴圈;
  • MFC Extension DLL:採用MFC動態連結版本建立,只用於MFC類庫的應用程式。

1、No-MFC DLL

動態連結庫通過匯出函式對外提供的介面,有兩種匯出函式的方法:a、通過模組定義(.ref)檔案宣告;b、通過關鍵字__declspec(dllexport)宣告匯出函式。這裡僅討論第二種方式,模組定義檔案的方式請自行查閱。

給出簡單的DLL建立方法,標頭檔案宣告瞭類LogBuilder和兩個匯出函式,其中CreateLogBuilder()函式為C風格函式。CPP檔案照常定義即可。

<LogBuilderDL.h>
class LogBuilder{ ... };
extern "C" __declspec(dllexport) LogBuilder* CreateLogBuilder(string path);
__declspec(dllexport) void DeleteLogBuilder(LogBuilder *lpLogBuilder);

1)顯示(動態)載入該DLL

<Main.cpp>
#include "../LogBuilderDL/LogBuilder.h"
typedef LogBuilder*(*CreatorByPath)(string);				// 巨集定義函式指標型別
int _tmain(int argc, _TCHAR* argv[])
{
	HINSTANCE hDll;						// DLL控制程式碼
	CreatorByPath creator;					// 函式指標
	hDll = LoadLibrary(L"..\\Debug\\LogBuilderDL.dll");
	if (hDll != NULL)
	{
		creator = (CreatorByPath)GetProcAddress(hDll, "CreateLogBuilder");
		if (creator != NULL)
		{
			LogBuilder* log = creator("log.log");
			log->WriteLog("Liwuqingxin", true);
		}
		FreeLibrary(hDll);
	}
	getchar();
	return 0;
}
  • 首先,載入DLL;
  • 然後,獲取了CreateLogBuilder()函式的地址;
  • 最後,通過函式地址呼叫該函式。

這裡需要注意兩點。

其一,以上DLL間接匯出了C++類,這裡通過C風格函式封裝類的獲取過程,獲取到類的例項後可正常使用該類,但類的靜態成員(需要使用域作用符訪問的成員)便無法匯出。另外可直接匯出C++類,第三點深入討論。當需要使用DLL中的型別、巨集定義或者變數時,需要包含該DLL的標頭檔案(顯式(動態)呼叫時,僅僅使用函式時並不需要)。

其二,CreateLogBuilder()為C風格函式。如果不宣告為extern "C",該函式被C編譯器編譯後在符號庫中的名字為"CreateLogBuilder",而C++編譯器則會產生名稱為"?CreateLogBuilder@@YAPAVLogBuilder@@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z"之類的外部連結符號(不同的編譯器可能生成的名字不同,但是都採用了相同的機制,生成的新名字稱為“mangled name”)[參考:http://www.jianshu.com/p/5d2eeeb93590]。顯示(動態)載入DLL時,GetProcAddress()函式需要通過上述真實的外部連結符號名稱去獲取函式地址,隱式(靜態)載入沒有影響。因此,匯出函式應使用extern "C"宣告為C風格函式更加合適(對載入方式沒有要求)。

其三,匯出C++類:在class關鍵字與類名中間新增匯出宣告(這裡需要使用巨集代替__declspec(dllexport),因為在呼叫DLL時需要宣告匯入類,直接使用該宣告則DLL使用者需要另外定義.h檔案)。這樣,DLL使用者可直接使用該類。但是靜態成員需要額外宣告為匯出。如下:

<LogBuilder.cpp>
API_DECLSPEC int LogBuilder::s = 0;
API_DECLSPEC int LogBuilder::fun()
{
	return 0;
}

並且在使用者使用時需要加上匯入lib的宣告:

<Main.cpp>
...
#pragma comment(lib, "../Debug/LogBuilderDL.lib")			// 使用類的靜態成員時需要
...

所謂的顯示(動態)載入,是通過windows API函式載入DLL,並獲取需要的函式地址,這個工作由API完成。而在客戶程式中直接使用類的靜態成員,編譯會報無法解析外部符號的錯,因為編譯器無法找到這些符號(未呼叫API),那麼我們只能自己顯示載入.lib檔案,並在DLL中宣告匯出靜態成員。更深入理解為,我們還可以直接將“?fun@LogBuilder@@SAHXZ”傳遞給GetProcAddress()函式獲取靜態成員的地址,這樣也能不載入.lib直接使用。

2)隱式(靜態)載入DLL

<Main.cpp>
#include "../LogBuilderDL/LogBuilder.h"
#pragma comment(lib, "LogBuilderDL.lib")
extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);

int _tmain(int argc, _TCHAR* argv[])
{
	LogBuilder *log = CreateLogBuilder("log.log");
	if (log != NULL)
		log->WriteLog("Liwuqingxin<span style="font-family: Arial, Helvetica, sans-serif;">", true);
	getchar();
	return 0;
}
  • 首先,包含DLL的標頭檔案;
  • 然後,告訴編譯器.lib檔案的路徑(方式和1中的靜態庫方法一致);
  • 再次,宣告匯入函式,對應於DLL匯出函式;
  • 最後,可以直接像正常函式一樣使用了。

需要注意幾點。

其一,CreateLogBuilder()為匯出函式,可以用來建立類的物件。若使用匯出類(前面有提到),還可以直接例項化該類(但是不推薦,會導致DLL HELL,後面詳述)。

其二,全域性變數需要宣告匯出,否則客戶程式包含標頭檔案後使用的全域性變數和DLL的中的全域性變數將是兩份副本!

其三,extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);這句宣告沒有似乎也可以呼叫該函式[參見:http://bbs.csdn.net/topics/330169671 ]。總結一下這裡查閱資料的收穫:

前文中,使用__declspec(dllexport)宣告匯出函式,這個方法沒錯,但是程式碼的寫法有些問題。明確一下:一個DLL建立後,需要提供給使用者的有三個檔案:.h、.lib、.dll。DLL建立者和使用者共用.h檔案,但需求不一樣:建立者需要宣告函式為__declspec(dllexport);使用者需要宣告函式為__declspec(dllimport)。因此,出於維護性和規範性考慮,使用預編譯巨集和巨集定義區分.h檔案的包含者:DLL自身加入預編譯巨集***_EXPORTING。否則,假如一個DLLA呼叫另一個DLLB而包含其標頭檔案時,將會使用__declspec(dllexport)而錯誤地將DLLB中匯入的函式作為DLLA的函式匯出了。(如此,Main.cpp中應該不用再加入extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);語句了)程式碼如下:

#ifdef HFILENAME_EXPORTING
#define API_DECLSPEC    __declspec(dllexport)
#else
#define API_DECLSPEC    __declspec(dllimport)
#endif

使用DLL時,__declspec(dllimport)宣告編譯時明確函式為從DLL匯入的外部函式,不需要間接定址,效率更高

3)DLL HELL

bz剛開始學習DLL相關,這裡參考:DLL匯出類。總結一下。[參考:http://m.blog.csdn.net/blog/guyue35/16996713]

1、DLL和客戶程式是分開編譯的,這會導致某些編譯時確定的內容在DLL中修改無法更新到客戶程式(除非你重新編譯客戶程式,這不現實)。以下情況會導致錯誤:

  • 應用程式直接訪問類的公有變數,而該公有變數在新DLL中定義的位置發生了變化;
  • 應用程式呼叫類的一個虛擬函式,而新的類中,該虛擬函式的前面又增加了一個虛擬函式;
  • 新類的後面增加了成員變數,並且新類的成員函式將訪問、修改這些變數;
  • 修改了新類的基類,基類的大小發生了變化;
  • 其他編譯時確定的內容,如C的常量(C++新特性常量為執行時確定),巨集等。

2、匯出類的大小、成員的位置等的改變無法通知到客戶程式,要想做一個可升級的DLL,以下三點用來使DLL遠離地獄:

不直接生成類的例項。對於類的大小,當我們定義一個類的例項,或使用new語句生成一個例項時,記憶體的大小是在編譯時決定的。要使應用程式不依賴於類的大小,只有一個辦法:應用程式不生成類的例項,使用DLL中的函式來生成。把匯出類的建構函式定義為私有的(privated),在匯出類中提供靜態(static)成員函式(如NewInstance())(靜態成員函式能夠用類名直接呼叫,而一般的成員函式要使用類物件來呼叫,這樣只有先宣告物件才能呼叫一般成員函式,此處要先用函式來構造類物件,故為靜態的)用來生成類的例項。因為NewInstance()函式在新的DLL中會被重新編譯,所以總能返回大小正確的例項記憶體。

不直接訪問成員變數。應用程式直接訪問類的成員變數時會用到該變數的偏移地址。所以避免偏移地址依賴的辦法就是不要直接訪問成員變數。把所有的成員變數的訪問控制都定義為保護型(protected)以上的級別,併為需要訪問的成員變數定義Get或Set方法。Get或Set方法在編譯新DLL時會被重新編譯,所以總能訪問到正確的變數位置。

忘了虛擬函式吧,就算有也不要讓應用程式直接訪問它。因為類的建構函式已經是私有(privated)的了,所以應用程式也不會去繼承這個類,也不會實現自己的多型。如果匯出類的父類中有虛擬函式,或設計需要(如類工場之類的框架),一定要把這些函式宣告為保護的(protected)以上的級別,併為應用程式重新設計呼叫該慮函式的成員函式。這一點也類似於對成員變數的處理。

事實上,建議你在釋出匯出類的DLL的時候,重新定義一個類的宣告,這個宣告可以不管原來的類裡的成員變數之類的,只把介面函式列在類的宣告裡。

[主要參考:《VC++動態連結庫(DLL)程式設計》 系列,作者:宋寶華,http://21cnbao.blog.51cto.com/109393/120777。

PS:本文參考了很多優秀的部落格、論壇等,感謝這些大蝦們的總結。在參考的地方基本上給出了原文連結。本文在此基礎上進行了實驗驗證並做了一些整理和總結。

相關文章