Windows下有靜態鏈接(lib)庫和動態鏈接庫(dll)兩種共享代碼的方式。
本文將介紹dll的應用場景,以及在vs2017平臺下的生成和使用。
動態鏈接庫dll
[What] dll是什麼
動態鏈接庫(Dynamic Link Library)又稱爲“應用程序擴展”,在windows系統中,大多數應用程序並非僅有一個可執行文件exe,同時也包含一些相對獨立(模塊化)的dll文件。dll中存放函數代碼實現,exe中存放dll中相應函數代碼的地址,而且dll中的代碼可以被多個exe調用而在內存中僅保留一份拷貝,從而節省了內存空間。
[How] 如何生成dll
1. 新建一個dll項目
選擇“具有導出項的(DLL)動態鏈接庫”,vs會幫我們自動創建與項目同名的.cpp文件和.h文件,並在.h文件中定義好相關導出符號;如果選擇“動態鏈接庫(DLL)”則不會創建上述文件。
創建完成後,可以看到vs已經幫我們完成導出符號和預處理器的定義:
2. dll兩種導出方式
vs官方文檔中提供了兩種方式可以導出dll中的函數:
-
模塊定義文件(.def):通用性(指給其他語言eg. Java、C#調用)好,但操作相對複雜;
-
關鍵字
__declspec(dllexport)
:操作簡單,但通用性較差。可見,vs創建dll項目時默認使用了該方式。
下面依次介紹這兩種導出方式。
3. 模塊定義文件(.def)
-
新建.def文件vs會自動添加.def文件爲鏈接器輸入:
-
實現一個dll函數
-
模塊定義語句規則
模塊定義語句有許多規則,這裏只用到
EXPORTS
關鍵字:
-
編寫.def文件如下:
4. 關鍵字__declspec(dllexport)
分別以C++和C的方式實現兩個導出函數,後面會看到這兩個函數的不同之處:
5. 生成結果
這裏除了MYDLL.dll
還生成了MYDLL.lib
文件,它是dll的導入庫,用於隱式鏈接dll。
從Visual Studio 命令提示符
處啓動DUMPBIN工具,執行dumpbin -exports path\to\dll
命令分析生成的dll,查看編譯器產生的函數修飾名:
可以得出以下兩個結論:
-
VC++編譯器會針對C++函數使用名稱修飾,而不會修飾以C方式聲明的函數(其實也修飾了,在函數名前面加了一個下劃線前綴,這是C語言的函數調用約定默認爲
__cdecl
所導致); -
.def文件的作用就是將被修飾過的C++函數重命名,使其可以被其他語言調用,具有通用性。
[How] 如何調用dll
新建一個控制檯應用,在其中調用上述生成的dll。
調用dll有兩種鏈接方式:隱式鏈接和顯式鏈接,無論哪種方式都要求將dll和exe放在同一目錄下。
1. 隱式鏈接
-
隱式鏈接需要三個文件:.h文件、.lib文件 和 .dll文件。
-
可以使用上一篇文章介紹的項目配置或編譯語句的方式將.h文件和.lib文件添加到項目中;如果dll項目和控制檯項目在同一解決方案下,也可以採取直接引用的方式將dll項目添加到控制檯項目。
-
這裏使用編譯語句的方式,調用結果如下:
2. 顯式鏈接
-
顯式鏈接只需要一個文件:.dll文件。
-
所謂顯式鏈接,就是直接調用WIN32 API函數
LoadLibrary
、GetProcAddress
和FreeLibrary
顯式地裝載、卸載dll。它們的作用如下: -
調用代碼:
#include <iostream>
#include <Windows.h> // 必要頭文件
void dllLinkExplicitly() {
typedef void(*LPFNDLLFUNC)(std::ostream &);
// equal to
// using LPFNDLLFUNC = void(*)(std::ostream &);
HINSTANCE hDLL; // Handle to DLL
LPFNDLLFUNC lpfnMyDLLWithDllExport; // Function pointer
LPFNDLLFUNC lpfnMyDLLWithExternC; // Function pointer
LPFNDLLFUNC lpfnMyDLLWithDefFile; // Function pointer
hDLL = LoadLibrary("MyDLL.dll");
if (hDLL != NULL) {
lpfnMyDLLWithDllExport = (LPFNDLLFUNC)GetProcAddress(hDLL, "fnMyDLLWithDllExport");
lpfnMyDLLWithExternC = (LPFNDLLFUNC)GetProcAddress(hDLL, "fnMyDLLWithExternC");
lpfnMyDLLWithDefFile = (LPFNDLLFUNC)GetProcAddress(hDLL, "fnMyDLLWithDefFile");
if (!lpfnMyDLLWithDllExport) { // handle the error
std::cout << "fnMyDLLWithDllExport load error.\n";
}
else { // call the function
lpfnMyDLLWithDllExport(std::cout);
}
if (!lpfnMyDLLWithExternC) {
std::cout << "fnMyDLLWithExternC load error.\n";
}
else {
lpfnMyDLLWithExternC(std::cout);
}
if (!lpfnMyDLLWithDefFile) {
std::cout << "fnMyDLLWithDefFile load error.\n";
}
else {
lpfnMyDLLWithDefFile(std::cout);
}
}
FreeLibrary(hDLL);
}
int main() {
dllLinkExplicitly();
std::cout << "this is my exe.\n";
system("PAUSE");
}
- 調用結果:
可以看出,由於fnMyDLLWithDllExport
的函數名被編譯器修飾過,已經無法通過原來的函數名調用。這也暴露出顯式鏈接的一個弊端:要求開發人員必須清楚地知道調用函數的導出名稱和傳參格式。
3. 小結
-
隱式鏈接是程序載入內存時加載所需的dll,且該dll隨主進程始終佔用內存。
-
優點:配置好.h文件和.lib文件的路徑後調用方式簡單直接;
-
缺點:需要藉助.h文件和.lib文件獲得所需函數的入口(如使用
#pragma comment
語句),注意這裏的lib是dll導入庫,與靜態鏈接庫lib有所不同。
-
-
顯式鏈接是在程序運行過程中需要時使用
LoadLibrary
加載,不需要時則使用FreeLibrary
將其卸載。如果加載時該dll已經在內存,則只需將其引用計數加1,如果其引用計數爲0則移出內存。-
優點:不需要.h文件和.lib文件,可以動態加載、卸載dll;
-
缺點:調用WIN32 API函數增加了編碼量,需要開發人員對dll很瞭解。
-
[Why] dll的優缺點
-
優點:相比lib加載全量代碼到exe中,dll節省了內存空間資源;當程序更新時,僅需要以補丁或擴展的形式發佈新的dll即可。
-
缺點:dll需隨exe一起發佈,否則程序在運行時會產生錯誤。
[Summary] lib和dll的比較
# | lib | dll |
---|---|---|
調用時需要的文件 | .h 和.lib |
隱式鏈接需要.h 、.lib 和.dll ,顯式鏈接僅需要.dll |
多次調用同一函數 | 內存中有多份拷貝 | 只有一份拷貝 |
能否再包含其他lib或dll | ❌ | ✔️ |
程序發佈是否需要提供 | ❌ | ✔️ |
程序後續更新 | 全量更新 | 增量更新 |
適用場景 | 一次性交付,不保證後續更新 | 持續性交付,系統有模塊化要求 |
[Github] 代碼
項目實例均在vs2017上測試,並上傳至GitHub: https://github.com/lyandut/EXE_LIB_DLL
[Reference] 參考
https://www.cnblogs.com/alantu2018/p/8470976.html
https://www.cnblogs.com/TenosDoIt/p/3203137.html
https://blog.csdn.net/weixin_43118068/article/details/88760001
https://blog.csdn.net/cynophile/article/details/79749524
https://blog.csdn.net/freeking101/article/details/104632710
https://blog.csdn.net/Hilavergil/article/details/78544424