動態鏈接庫(DLL)的生成與使用

1.動態鏈接庫(dll)概述

沒接觸dll之前覺得它很神祕,就像是一個黑盒子,既不能直接運行,也不能接收消息。它們是一些獨立的文件,其中包含能被可執行程序或其他dll調用來完成某項工作的函數,只有在其他模塊調用dll中的函數時,dll才發揮作用。 
在實際編程中,我們可以把完成某項功能的函數放在一個動態鏈接庫裏,然後提供給其他程序調用。像Windows API中所有的函數都包含在dll中,如Kernel32.dll, User32.dll, GDI32.dll等。那麼dll究竟有什麼好處呢?

1.1 靜態庫和動態庫

靜態庫:函數和數據被編譯進一個二進制文件(擴展名通常爲.lib),在使用靜態庫的情況下,在編譯鏈接可執行文件時,鏈接器從靜態庫中複製這些函數和數據,並把它們和應用程序的其他模塊組合起來創建最終的可執行文件(.exe)。當發佈產品時,只需要發佈這個可執行文件,並不需要發佈被使用的靜態庫。
動態庫:在使用動態庫時,往往提供兩個文件:一個引入庫(.lib,非必須)和一個.dll文件。這裏的引入庫和靜態庫文件雖然擴展名都是.lib,但是有着本質上的區別,對於一個動態鏈接庫來說,其引入庫文件包含該動態庫導出的函數和變量的符號名,而.dll文件包含該動態庫實際的函數和數據。

1.2 使用動態鏈接庫的好處

可以使用多種編程語言編寫:比如我們可以用VC++編寫dll,然後在VB編寫的程序中調用它。
增強產品功能:可以通過開發新的dll取代產品原有的dll,達到增強產品性能的目的。比如我們看到很多產品踢動了界面插件功能,允許用戶動態地更換程序的界面,這就可以通過更換界面dll來實現。
提供二次開發的平臺:用戶可以單獨利用dll調用其中實現的功能,來完成其他應用,實現二次開發。
節省內存:如果多個應用程序使用同一個dll,該dll的頁面只需要存入內存一次,所有的應用程序都可以共享它的頁面,從而節省內存。

2. dll的創建

dll的創建主要有兩種方法:一是使用 __declspec(dllexport) 創建dll,二是使用模塊定義(.def)文件創建dll。

2.1 使用 __declspec(dllexport) 創建dll

首先在VS中的Visual C++中創建一個Win32 Project,取名爲Dll1。在Application Type中選擇DLL,在Additional options中選擇Empty project,即創建一個空的動態鏈接庫工程。 

                    
 
然後爲工程添加一個C++源文件:Dll1.cpp,假設我要實現的是加法和減法運算,則代碼如下:

__declspec(dllexport) int add(int a, int b){
    return a + b;
}

__declspec(dllexport) int subtract(int a, int b){
    return a - b;
}

爲了讓dll導出函數,需要在每一個需要被導出的函數前面加上標識符:__declspec(dllexport)。 
利用Build命令生成Dll1動態鏈接庫,這時在Dll1/Debug目錄下就會生成.dll文件和.lib文件,這兩個文件即爲所需的動態鏈接庫的文件。 

                    
 
既然已經有了這個dll文件,是不是就可以在其他程序中訪問該dll中的add和subtract函數了呢?必須注意的一點是:應用程序如果想要訪問某個dll中的函數,那麼這個函數必須是已經被導出的函數。 
爲了查看一個dll中有哪些導出函數,Visual Studio提供了一個命令行工具:Dumpbin。

2.2 使用Dumpbin命令確認dll的導出函數

首先在命令行中進入到VS的安裝目錄下,運行一個名爲VCVARS32.bat的批處理程序(對於VS2013來說,該bat文件位於\VC\bin目錄下),該文件的作用是用來創建VC++使用的環境信息。(注意,當在命令行界面執行VCVARS32.bat文件後,該文件設置的環境信息只在當前命令行窗口生效。) 
然後輸入dumpbin命令,即可列出該命令的使用方法: 

                    
 
那麼想要查看一個dll提供的導出函數,在Dll1.dll文件所在目錄下,在命令行中輸入下述命令:

dumpbin -exports Dll1.dll

                    

在上圖中可以看到我們導出了兩個函數,但是導出函數的名稱長得很奇怪,add導出函數的名稱是“?add@@YAHHH@Z”,subtract導出函數的名稱是“?subtrct@@YAHHH@Z”。這是因爲在編譯鏈接時,C++會按照自己的規則篡改函數的名稱,這一過程稱爲“名字改編”。這會導致不同的編譯器、不同的語言下調用dll發生問題。因此我們希望動態鏈接庫文件在編譯時,導出函數的名稱不要發生變化。 
爲了實現這一目的,可以再定義導出函數時加上限定符:extern “C”,如

extern "C" __declspec(dllexport) int add(int a, int b){
//...
}

但是這種方式只能解決C++和C語言之間相互調用時函數命名的問題。爲了徹底解決這個問題,可以通過模塊定義(.def)文件實現。

2.3 使用模塊定義(.def)文件創建dll

使用def文件創建dll的話就不再需要__declspec(dllexport),因此將代碼寫成最原始的樣子:

int add(int a, int b){
    return a + b;
}

int subtract(int a, int b){
    return a - b;
}

同時爲工程創建一個後綴名爲.def的文件,並添加進工程,編輯其內容爲:

LIBRARY Dll1

EXPORTS
add
subtract

其中LIBRARY語句用於指定動態鏈接庫的名稱,該名稱與生成的動態鏈接庫名稱一定要匹配。EXPORTS語句用於表明dll將要導出的函數,以及爲這些導出函數指定的符號名。 
將該模塊定義文件鏈接到工程中,方法爲工程屬性頁面>鏈接器>輸入>模塊定義文件中寫入“Dll1.def”。 

                    
 
然後重新Build Solution,並用dumpbin工具查看現在dll導出的函數,可以發現函數的名字改編問題得到了解決! 

                    
以上就是創建dll的兩種方法,個人比較提倡使用模塊定義(.def)文件創建dll,代碼簡潔的同時還沒有名字改編的問題。 
接下來我們來看看如何使用創建好的dll。

3. dll的使用

dll的使用也有兩種方法,一是隱式鏈接的方式加載dll,二是顯示加載方式加載dll。

3.1 隱式鏈接方式加載dll

爲了更好地展示dll的使用,我首先創建了一個基於MFC的對話框程序,然後爲其添加兩個按鈕。 
                     
將生成好的Dll1.dll和Dll1.lib複製到對話框程序所在的文件夾,然後在CXXXDlg.h中註冊動態鏈接庫的引入庫文件。因爲.lib文件包含了Dll1.dll中導出函數的符號名,相當於告訴對話框程序相關函數應該去dll中調用。

#pragma comment(lib,"Dll1.lib")

然後在CXXXDlg.cpp中聲明外部函數:

_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);

這樣我們就可以使用這兩個函數了。爲兩個按鈕添加事件響應程序,並添加如下代碼:

void CXXXDlg::OnBtnAdd()
{
    // TODO: Add your control notification handler code here
    CString str;
    str.Format(_T("5 + 3 = %d"), add(5, 3));
    MessageBox(str);
}

運行程序發現可以通過dll的導出函數來實現加法功能了。說明dll可以使用。 

             
3.2 顯示加載方式加載dll

另一種是通過LoadLiabrary函數顯示加載dll。代碼如下。需要注意的是這時候我們不再需要註冊.lib文件,也不需要聲明外部函數。只要在需要使用的地方調用dll文件即可。

void CXXXDlg::OnBtnSubtract()
{
    // TODO: Add your control notification handler code here
    HINSTANCE hInst;
    hInst = LoadLibrary(L"Dll1.dll");
    typedef int(*SUBPROC)(int a, int b);
    SUBPROC Sub = (SUBPROC)GetProcAddress(hInst, "subtract");
    CString str;
    str.Format(_T("5-3=%d"), Sub(5, 3));
    FreeLibrary(hInst);       //LoadLibrary後要記得FreeLibrary
    MessageBox(str);
}

                    

3.3 兩種加載方式對比

通過以上的例子,可以看到隱式鏈接和動態加載兩種加載dll的方式各有優點。

隱式鏈接方式實現簡單,一開始就把dll加載進來,在需要調用的時候直接調用即可。但是如果程序要訪問十多個dll,如果都採用隱式鏈接方式加載他們的話,在該程序啓動時,這些dll都需要被加載到內存中,並映射到調用進程的地址空間,這樣將加大程序的啓動時間。而且一般來說,在程序運行過程中只是在某個條件滿足的情況下才需要訪問某個dll中的函數,如在上述例子中,我只有在點擊按鈕時才需要訪問dll,其他情況下並不需要訪問。這樣如果所有dll都被加載到內存中,資源浪費是比較嚴重的。
顯示加載的方法則可以解決上述問題,dll只有在需要用到的時候纔會被加載到內存中。另外,其實採用隱式鏈接方式訪問dll時,在程序啓動時也是通過調用LoadLibrary函數加載該進程需要的動態鏈接庫的。
————————————————
版權聲明:本文爲CSDN博主「Elaine_Bao」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/elaine_bao/article/details/51784864

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章