假設被調用的DLL存在一個導出函數,原型如下:
void printN(int);
1|0三種方式從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 ?件中的注釋由每個注釋?開始處的分號 ( 指定,且注釋不能與語句共享??。
2|0編寫dll注意點
編寫dll時,有個重要的問題需要解決,那就是函數重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼里解決采用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一種是采用def文件。
2|1編寫dll時,為什么有 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,在代碼里給函數別名。
2|2_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],這樣就不會有多余的跳轉了。
2|3__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")
3|0編寫測試dll代碼
項目結構:
cpp源代碼:
#include <IOStream>
using namespace std;
extern "C" {
_declspec(dllexport) void printN(int n)
{
//printf("%dn", n);
cout << n << endl;
}
}
void printM(int m)
{
cout << m << endl;
}
#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")
int getNresult()
{
//printf("%dn", 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文件。
4|0編寫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)導入,不過導入函數的情況下可以省略不寫,引用外部變量則不能省略。
執行結果:
5|0利用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: