假設被調用的DLL存在一個導出函數,原型如下:
void printN(int);
複制
三種方式從DLL導入導出函數
- 生成
時使用子產品定義 (DLL
) 檔案.def
- 在主應用程式的函數定義中使用關鍵字
或__declspec(dllimport)
__declspec(dllexport)
- 利用
#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"
def編寫規範:參考子產品定義 (.Def) 檔案
基本規則:
- LIBRARY 語句說明 .def ⽂件相應的 DLL;
- EXPORTS 語句後列出要導出函數的名稱。可以在 .def ⽂件中的導出函數名後加 @n,表 示要導出函數的序号為 n(在進⾏函數調⽤時,這個序号将發揮其作⽤);
- .def ⽂件中的注釋由每個注釋⾏開始處的分号 (? 指定,且注釋不能與語句共享⼀⾏。
編寫dll注意點
編寫dll時,有個重要的問題需要解決,那就是函數重命名——
Name-Mangling
。解決方式有兩種,一種是直接在代碼裡解決采用
extent”c”
、
_declspec(dllexport)
、
#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]")
,另一種是采用
def
檔案。
編寫dll時,為什麼有 extern “C”
extern “C”
原因:因為C和C++的重命名規則是不一樣的。這種重命名稱為
“Name-Mangling”(
名字修飾或名字改編、辨別符重命名,有些人翻譯為“名字粉碎法”,這翻譯顯得有些莫名其妙)
據說,C++标準并沒有規定
Name-Mangling
的方案,是以不同編譯器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的編譯器他們的Name-Mangling規則也是不同的。這樣的話,不同編譯器編譯出來的目标檔案.obj 是不通用的,因為同一個函數,使用不同的Name-Mangling在obj檔案中就會有不同的名字。如果DLL裡的函數重命名規則跟DLL的使用者采用的重命名規則不一緻,那就會找不到這個函數。
影響符号名的除了C++和C的差別、編譯器的差別之外,還要考慮調用約定導緻的Name Mangling。如
extern “c” __stdcall
的調用方式就會在原來函數名上加上寫表示參數的符号,而
extern “c” __cdecl
則不會附加額外的符号。
dll中的函數在被調用時是以函數名或函數編号的方式被索引的。這就意味着采用某編譯器的C++的Name-Mangling方式産生的dll檔案可能不通用。因為它們的函數名重命名方式不同。為了使得dll可以通用些,很多時候都要使用C的Name-Mangling方式,即是對每一個導出函數聲明為extern “C”,而且采用_stdcall調用約定,接着還需要對導出函數進行重命名,以便導出不加修飾的函數名。
注意到
extern “C”
的作用是為了解決函數符号名的問題,這對于動态連結庫的制造者和動态連結庫的使用者都需要遵守的規則。
動态連結庫的顯式裝入就是通過
GetProcAddress
函數,依據動态連結庫句柄和函數名,擷取函數位址。因為
GetProcAddress
僅是作業系統相關,可能會操作各種各樣的編譯器産生的dll,它的參數裡的函數名是原原本本的函數名,沒有任何修飾,是以一般情況下需要確定dll裡的函數名是原始的函數名。分兩步:
一,如果導出函數使用了
extern”C” _cdecl
,那麼就不需要再重命名了,這個時候dll裡的名字就是原始名字;如果使用了
extern”C” _stdcall
,這時候dll中的函數名被修飾了,就需要重命名。
二、重命名的方式有兩種,要麼使用
*.def
檔案,在檔案外修正,要麼使用
#pragma
,在代碼裡給函數别名。
_declspec(dllexport)
和 _declspec(dllimport)
的作用
_declspec(dllexport)
_declspec(dllimport)
_declspec
還有另外的用途,這裡隻讨論跟dll相關的使用。正如括号裡的關鍵字一樣,導出和導入。
_declspec(dllexport)
用在dll上,用于說明這是導出的函數。而
_declspec(dllimport)
用在調用dll的程式中,用于說明這是從dll中導入的函數。
因為dll中必須說明函數要用于導出,是以
_declspec(dllexport)
很有必要。但是可以換一種方式,可以使用
def
檔案來說明哪些函數用于導出,同時def檔案裡邊還有函數的編号。
而使用
_declspec(dllimport)
卻不是必須的,但是建議這麼做。因為如果不用
_declspec(dllimport)
來說明該函數是從dll導入的,那麼編譯器就不知道這個函數到底在哪裡,生成的exe裡會有一個
call XX
的指令,這個XX是一個常數位址,XX位址處是一個
jmp dword ptr[XXXX]
的指令,跳轉到該函數的函數體處,顯然這樣就無緣無故多了一次中間的跳轉。如果使用了
_declspec(dllimport)
來說明,那麼就直接産生
call dword ptr[XXX]
,這樣就不會有多餘的跳轉了。
__stdcall
帶來的影響
__stdcall
這是一種函數的調用方式。預設情況下VC使用的是
__cdecl
的函數調用方式,如果産生的dll隻會給C/C++程式使用,那麼就沒必要定義為
__stdcall
調用方式,如果要給Win32彙編使用(或者其他的__stdcall調用方式的程式),那麼就可以使用__stdcall。這個可能不是很重要,因為可以自己在調用函數的時候設定函數調用的規則。像VC就可以設定函數的調用方式,是以可以友善的使用win32彙編産生的dll。不過
__stdcall
這調用約定會
Name-Mangling
,是以我覺得用VC預設的調用約定簡便些。但是,如果既要__stdcall調用約定,又要函數名不給修飾,那可以使用*.def檔案,或者在代碼裡#pragma的方式給函數提供别名(這種方式需要知道修飾後的函數名是什麼)。
舉例:
·extern “C” __declspec(dllexport) bool __stdcall cswuyg();
·extern “C”__declspec(dllimport) bool __stdcall cswuyg();
·#pragma comment(linker, "/export:cswuyg=_cswuyg@0")
複制
編寫測試dll代碼
項目結構:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2Pn5Gcuc2dstWNkhTOll3LcNTO5YjN4MzLcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
cpp源代碼:
#include <iostream>
using namespace std;
extern "C" {
_declspec(dllexport) void printN(int n)
{
//printf("%d\n", n);
cout << n << endl;
}
}
void printM(int m)
{
cout << m << endl;
}
#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")
int getNresult()
{
//printf("%d\n", n);
return 123;
}
複制
def代碼:
LIBRARY DLLTEST
EXPORTS
printM
複制
項目屬性中将配置類型改為dll:
子產品定義檔案改為dlltest.def:
編譯之後,使用CFF Explorer檢視導出函數:
其中
printN
函數用
extern "C" _declspec(dllexport)
的方式導出,避免了函數名粉碎;
printM
函數用
def
的形式導出,也避免了函數名粉碎;
getNresult
函數用
#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")
的形式避免了函數名粉碎,但是需要知道粉碎後的原始函數符号;
這裡涉及一個問題,原始函數符号怎麼找到的,方法是先用
_declspec(dllexport)
方式導出,然後編譯後利用CFF即可看到原始函數符号。
編譯dll後會産生一個dll檔案和一個lib檔案,如果是運作時動态調用的方式隻使用dll檔案就行,如果要在編譯時以庫的形式提供給exe調用則需要lib檔案。
編寫exe調用dll
項目結構:
cpp源碼:
#include <iostream>
using namespace std;
#pragma comment(lib, "C:\\project\\dlltest\\Debug\\dlltest.lib")
extern "C" __declspec(dllimport) void printN(int);
int getNresult();
void printM(int);
int main()
{
printN(123);
printM(12);
cout << getNresult() << endl;
return 0;
}
複制
在
#pragma
中更改為自己的lib路徑,
printN
以
extern "C" __declspec(dllimport)
形式導入,
getNresult
和
printM
是c++格式的,應該使用
__declspec(dllimport)
導入,不過導入函數的情況下可以省略不寫,引用外部變量則不能省略。
執行結果:
利用LoadLibrary動态加載dll的方式
這種方式需要明确指定dll的位置,而不是程式根據環境變量配置自己尋找(上面的方式中并沒有指明dll的位置,exe和dll同目錄會自動搜尋加載)。
代碼:
#include <iostream>
#include <Windows.h>
using namespace std;
int main()
{
HINSTANCE h = LoadLibrary(L"C:\\project\\dlltest\\Debug\\dlltest.dll");
if (h == NULL)
{
cout << "dll加載失敗!" << endl;
}
else
{
void* func = GetProcAddress(h, "printN");
if (func != NULL)
{
((void(*)(int))func)(2);
}
else
{
cout << "未找到相關函數!" << endl;
}
}
return 0;
}
複制
需要注意将項目的字元集改為
Unicode
: