c++動態庫生成與調用
一、生成動態庫(含頭文件、不含頭文件)
以生成dllTest.dll為例(工程名為dllTest、 頭文件名為dllTest.h、 源文件名為dllTest.cpp)
1.1 不含頭文件的動態庫
我們生成的動態庫想要被別人調用,那麼一定要將想要被調用的函數導出,使用_declspec(dllexport)進行導出。
//dllTest.cpp
_declspec(dllexport) int add(int a, int b)
{
return a+b;
}
_declspec(dllexport) int sub(int a, int b)
{
return a-b;
}
編譯之後,在工程目錄的Debug目錄下我們可以看到以下的幾個文件,分別是
得到上述的文件之後,我們就可以進行調用該動態鏈接庫了。其中,dll文件是包含了函數具體實現的可執行文件;lib文件是導入庫文件,主要包含的是函數名和符號名。我們可以用vs提供的dumpbin工具查看生成的動態庫中導出的函數以及名字。我的vs安裝在C:\Program Files (x86)\Microsoft Visual Studio 10.0,進入該文件夾後點擊VC進入該目錄,可以看到一個vcvarsall.bat的文件。依次點擊Microsoft Visual Studio 2010 -> visual studio tools ->Visual Studio Prompt 2010,如下圖所示:
即可打開一個vs的命令框,將vcvarsall.bat文件拖拽進入該命令框中,將得到下圖所示的內容:
然後我們將得到的dllTest.dll文件放入到VC文件夾中,輸入dumpbin -exports dllTest.dll即可看到該動態庫導出的函數以及經過c++編譯器修飾後的函數名字。
橢圓所示的即為導出的函數以及其經過修飾後的名字。(因為c++要支持函數重載以及命名空間等,因此需要將函數進行修飾)。
1.2 動態庫調用(隱式連接、動態連接)
1.2.1 隱式連接
首先建立一個工程,配置工程屬性,具體的步驟如下:
(1)新建一個win32控制台應用程序,工程名dllCall;
(2)配置工程屬性,添加對動態庫dllTest.lib的引用:
a. 工程項目右鍵->屬性->鏈接器->gengeral->附加庫目錄,在其中添加導入庫dllTest.lib所在的文件目錄,如下圖:
b.工程項目右鍵->屬性->鏈接器->輸入->附加依賴項,在其中添加導入庫dllTest.lib,如下圖:
(3)此時,我們就可以在我們的工程中對該動態庫進行調用了。
調用函數dllCal.cppl如下:
// dllCall.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include using namespace std; extern int add(int, int ); //告訴編譯器,add函數是在該源文件外部定義的函數 _declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數 //這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些 int _tmain(int argc, _TCHAR* argv[]) { int a = 5, b = 3; cout< (4)編寫好之後,編譯、鏈接都能通過,但是運行的時候會出錯,因為調用函數不知道int add(int, int ) 以及int sub(int, int)的可執行文件(dllTest.dll)在哪,因此當調用該函數時,就找不到具體的執行的代碼,因而就會報錯。此時,我們只需將dllTest.dll放入到調用函數的.exe文件所在的目錄中即可,結果如下圖:
上述的兩種形式都能正常的調用。
1.2.2 動態調用
上述的隱式調用需要我們在工程屬性中配置一些動態庫導入庫(dllTest.lib)的目錄以及名稱,會很麻煩,有時候我們也會忘記配置這些屬性或者當動態庫較多的時候有遺漏,都會導致函數鏈接的時候出現unresolve external symbol的錯誤。而且,動態調用還有一個優點就是,什麼時候需要調用動態庫的函數的時候什麼時候加載該動態庫,這樣就不必在程序運行開始時加載所需的所有的動態庫,這樣也能加快啟動的速度。此外,我們僅僅只需要一個dllTest.dll文件即可。
(1)我們首先刪掉工程屬性中 “附加庫目錄”以及“附加依賴項”中我們輸入的內容。
(2)修改相應的調用代碼,如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
using namespace std;
//extern int add(int, int ); //告訴編譯器,add函數是在該源文件外部定義的函數
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數
//這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
HINSTANCE hInst = LoadLibraryA("dllTest.dll");
typedef int (*pFun)(int, int);//定義一個函數指針類型pAdd
pFun add = (pFun)GetProcAddress(hInst,"?add@@YAHHH@Z");
cout<
源文件:
//dllTest.cpp #define DLLTEST_API _declspec(dllexport) #include "dllTest.h" DLLTEST_API int add(int a, int b) { return a+b; } DLLTEST_API int sub(int a, int b) { return a-b; }
2.1 函數調用(隱式鏈接、動態鏈接)
2.1.1 隱式鏈接
(1) 新建工程,配置工程屬性(添加導入庫目錄、導入庫),具體的參照上面的隱式調用。此外,還要將動態庫頭文件所在的目錄加入到屬性中: 在 c\c++ -> gengeral -> additional include Directories 加入dllTest.h所在的目錄。 如下圖:
(2)修改調用函數,具體的代碼如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include "dllTest.h"
using namespace std;
//extern int add(int, int ); //告訴編譯器,add函數是在該源文件外部定義的函數
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數
//這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include "dllTest.h"
#pragma comment(lib,"dllTest.lib")
using namespace std;
//extern int add(int, int ); //告訴編譯器,add函數是在該源文件外部定義的函數
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數
//這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<3.1 extern "C"
3.1.1 默認調用方式_cedel
以含頭文件的動態庫為例進行修改,修改後的頭文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else
#define DLLTEST_API extern "C" _declspec(dllimport)
#endif
DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);
//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"
DLLTEST_API int add(int a, int b)
{
return a+b;
}
DLLTEST_API int sub(int a, int b)
{
return a-b;
}
進行編譯得到相應的.dll、.lib文件,通過dumpbin工具,我們可以查看導出函數的名字,如下:
紅色部分為 沒有加extern "C" 導出的函數名,它經過了c++編譯器的修飾。
綠色部分為添加了extern "C" 導出的函數名,它的意思是告訴編譯器,以C的方式導出函數。
3.1.2 更改調用方式 _stdcall
但是如果改變了調用方式,通過這種方式進行導出,函數的名字依舊會改變,我們采用_stdcall調用方式,也就是
WINAPI,後面講為什麼window API 函數都采用該種調用方式:
(1)編寫_stdcall調用方式的動態庫(以含頭文件方式為例)
相應的頭文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else
#define DLLTEST_API extern "C" _declspec(dllimport)
#endif
DLLTEST_API int _stdcall add(int, int);
DLLTEST_API int _stdcall sub(int, int);
//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"
DLLTEST_API int _stdcall add(int a, int b)
{
return a+b;
}
DLLTEST_API int _stdcall sub(int a, int b)
{
return a-b;
}
編譯後利用dumpbin工具查看導出函數的名字如下:
紅色的為采用c\c++默認調用方式導出的函數名字;
綠色的為采用_stdcall 調用方式導出的函數名字。至於為什麼會是這樣的名字在後面的名字修飾規則中進行說明。我們發現他的名字還是改變了。
3.2 模塊定義文件.def
3.2.1 默認調用方式(_cedel)動態庫
(1)新建一個Win32程序,選擇一個動態庫程序,勾選空工程。
(2)修改相應的代碼(源文件、模塊定義文件),具體的如下:
源文件:
//dllTest.cpp
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}
模塊定義文件:
LIBRARY dllTest
EXPORTS
add
sub
其中,LIBRARY 後面要與生成的動態庫的名稱相同,EXPORTS下面寫需要導出的函數(add、sub),它會自動與你的源文件中的函數進行匹配。也可以用 add1 = add、 sub1 = sub 這樣的方式來改變導出函數的名字。
(3)通過dumpbin工具查看動態庫的導出函數,如下圖(分別是沒改名字以及改名字後的):
3.2.2 采用_stdcall,也就是WINAPI調用方式生成動態庫
(1) 源文件
//dllTest.cpp
int _stdcall add(int a, int b)
{
return a+b;
}
int _stdcall sub(int a, int b)
{
return a-b;
}
(2)模塊定義文件
LIBRARY dllTest
EXPORTS
add
sub
(3)利用dumpbin查詢動態庫導出的函數
3.3 動態庫調用
3.3.1 動態調用
動態調用同上,只是變了一個地方,如下:
pFun add = (pFun)GetProcAddress(hInst,"add");
pFun sub = (pFun)GetProcAddress(hInst,"sub");
也可以使用每個函數前面的編號,如add函數的編號為1 、sub函數的編號為2。只需要將上述函數的第二個參數改為
MAKEINTATOM(1)。
3.3.2 隱式調用
<1> 動態庫與函數庫都采用_cedel調用方式
(1) 配置工程屬性,加入附加庫目錄;(附加依賴項使用#pragma comment(lib,"testdll.lib"代替)
(2)調用函數如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#pragma comment(lib,"testdll.lib")
using namespace std;
_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<(2)編寫調用函數,如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#pragma comment(lib,"testdll.lib")
using namespace std;
_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<
這是因為_stdcall是被調用的函數自己清理棧空間,而_cedel則是調用者清理棧。上述的調用方式會使棧得到兩次清理,使得函數的返回地址、ebp寄存器的值被更改從而導致失敗。通過觀察其生成的匯編語言就可以看到(下面的主要的,並不是全部):
main函數在調用add函數之後清理的棧,而在add函數中,可以清楚的看到,add函數在運行完之後自己清理的棧。
因此,導致棧數據出現錯誤。