動態庫編程詳解

 

目錄

概述

一、動態庫概念與分類

1、什麼是動態庫

2、動態庫分類

4、動態庫解決的問題

二、動態庫的創建

1、規則動態庫

2、聲明導出函數的兩種方式

2.1__declspec(dllexport)導出

2.2 .def文件導出

3、導出導入類

三、隱式、顯示調用動態庫

1、動態庫隱式調用

2、動態庫顯示調用

3.顯示、隱式調用的區別

四、動態庫的測試

 

 

 

概述

      動態庫是繼靜態庫發展起來的一種封裝重用技術,在靈活性、擴展性、重用性各方面取得了突破,本文只介紹動態庫的基礎知識,關於動態庫的高級編程,推薦參看Jeffrey ReichterChiristophe Nasarre合著的《Windows核心編程》第5版。

      

一、動態庫概念與分類

1、什麼是動態庫

   DLL(Dynamic Linkable Library)動態鏈接庫亦簡稱動態庫,它是一塊封裝好的代碼塊,包含着一些方法,一般不包括消息循環,也建議不要去包含這些。可把它看成一個倉庫,其提供了可直接使用的變量、函數、類等。打個不太生動的比喻,動態庫猶如保衛森嚴的生產基地,但你可以通過正確入口進入,獲得你想要的東西,你不用管也管不着這東西是怎麼生產的,拿走從出口出來就行,同時生產基地是共享的,大家都可以通過入口獲得相應的東西。

      在“庫”的發展史上經歷了“無庫---靜態庫---動態庫”的時代,無論是動態庫還是靜態庫都能解決代碼共享的問題。

動態庫是基於二進制級重用的,所以與語言無關、環境無關(前提是你動態庫中沒有涉及對環境有依賴的東西,如調用一些第三方DLL)的,再一個得遵循DLL接口規範和調用約定,簡而言之,用各種語言編寫的標準DLL其他語言都可以調用。所以如果想創建一個通用的DLL,那麼得嚴格遵守DLL規範,包括導出、調用約定、形參幾方面的內容。

 

2、動態庫分類 

通過VC++工具編寫的動態庫分爲兩類----規則DLL與非規則動態庫,Visual C++支持編寫三種DLL,它們分別是Non-MFC DLL(非MFC動態庫)、MFC Regular DLLMFC規則DLL)、MFC Extension DLLMFC擴展DLL)。非MFC動態庫不採用MFC類庫結構,其導出函數爲標準的C接口,能被非MFCMFC編寫的應用程序所調用;MFC規則DLL包含一個繼承自CWinApp的類,但其無消息循環;MFC擴展DLL採用MFC的動態鏈接版本創建,它只能被用MFC類庫所編寫的應用程序所調用。

4、動態庫解決的問題

      節省資源:如果採用靜態鏈接庫,則無論你願不願意,lib中的指令都被直接包含在最終生成的EXE文件中了。但是若使用DLL,該DLL不必被包含在最終EXE文件中,EXE文件執行時可以動態地引用和卸載這個與EXE獨立的DLL文件。

      節省內存,假如本地有多個進程用到動態庫,那麼在內存只是只載入一次的,兩個進程共用該DLL在內存中的頁面。

靈活性:我認爲靈活性纔是動態庫最值得稱道的地方,發行的動態庫,只要原有的接口不改變,那麼你可以任意地改動,增加動態庫裏面的內容,同時以新版本替換舊版本,而不影響依賴於此動態庫的程序。舉個例子吧,例如你的一個軟件已經發行了,如果出了什麼問題,只要找出出問題的模塊(假如它就是個動態庫),並處理好,把新版的動態庫給用戶替換舊版的就OK了,如果全是靜態庫呢?那只有把整個工程編譯一編,發一個大包給人家,人家也許還要把以前的軟件卸載,重新裝一次新的版本。

模塊化:這對大項目特別有利,利用動態庫可以把項目切割成N個小塊加以分工,還有利於錯誤定位等,豈不快哉。

語言無關性:只要遵循約定的DLL接口規範和調用方式,用各種語言編寫的DLL其他語言都可以相互調用。

特殊用途:windows提供的一些特性只有通過DLL才用使用,如鉤子(hook)就需要通過DLL來實現,關於鉤子的編程細節,將在後續的博文中詳細介紹。

 

二、動態庫的創建

1、規則動態庫

   VC++6.0爲開發工具,在VC++中創建Win32 Dynamic-Link Library工程,下圖中選擇第一項,當然也可以選擇其他項。

在創建的工程中創建一個頭文件MyDll.h與一個CPP文件MyDll.cpp,在頭文件與源文件中寫入以下代碼:

//MyDll.h

#ifndef MYDLLEIPORT

#define MYDLLEIPORT extern "C" __declspec(dllimport)

#endif

MYDLLEIPORT int add(int a, int b);

MYDLLEIPORT int     g_nCount;

 

定義實體:

#define MYDLLEIPORT extern "C" __declspec(dllexport)

#include "MyDll.h"

int g_nCount = 0;

int add(int a, int b){

      return (g_nCount += a + b);

}

代碼寫完了,現在的工作是怎麼把函數導出的問題了,下面分析如何導出函數,與導出的方式

2、聲明導出函數的兩種方式

DLL中導出函數的聲明有兩種方式:一種是在函數聲明中加上__declspec(dllexport)調用DLL時配合.lib文件通過__declsped(dllimport)指令導入函數即可,另外一種方式是採用模塊定義(.def)文件聲明,.def文件爲鏈接器提供了有關被鏈接程序的導出、屬性及其他方面的信息。

通過修飾符__declspec(dllexport)導出的函數,可能會面臨函數名改編的問題,而通過.def文件導出的,將不會有此問題,要導出編譯時函數名不會被改編的函數,還有一種方法,就是在DLL源文件中輸入以下代碼:

#pragma comment(linker, “/export:add=_add@8”)

不過此方法有個麻煩事,你得事先知道函數改篇後的名字是什麼,如函數add改編後是_add@8,還有此方法導出的函數名是兩個,”add””_add@8”,個人建議使用.def文件導出最爲方便快捷,省去非常多的麻煩。

      這兩種導出方式都會生成一個與Dll同名的.lib文件,此文件在隱式調用時得用。.lib文件只是包含一些導出函數的屬性說明信息。

2.1__declspec(dllexport)導出

   在頭文件聲明函數前加上__declspec(dllexport),如下:

#ifndef MYDLLEIPORT

#define MYDLLEIPORT extern "C" __declspec(dllimport)

#endif

MYDLLEIPORT int add(int a, int b);//導出函數

MYDLLEIPORT int g_nCount;//導出變量

其中extern"C"表示函數是安C語言的方式編譯,C++對函數名稱編譯時會改編如add,C++編譯後變成_add@8,改名後,C語言或者其他語言調用的時候會出現問題,會找不到,同時不同廠家的編譯器編譯時生成的函數名也可能不一樣; __declspec(dllexport)是導出修飾符,指明導出此函數,不用過分解釋,照用就行;下面是對extern "C"的解釋:

extern "C"包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”,表示可被外部文件引擎;其次,被它修飾的目標是“C”的,表示按照C語言方式編譯和連接。

編譯規(extern "C")必須指定,才能編譯出規範的DLL接口。用這個方法導出接口,如果用VC++編譯,還是會有函數名改編的問題,編譯後,函數名應該是_add@8,調用DLL時,如果用同一廠家(VC++)編譯器,是不是會有問題的,用其他廠家的編譯器,那就不能保證了,所以這種導出方法並不是很好,可採用下面介紹的通過.def文件導出的方法。

2.2 .def文件導出

      此方法導出函數,將避免函數名改編的問題。在工程中添加一.def文件---MyDll.def:

在新加入的MyDll.def文件中輸入以下代碼:

;MyDll.def :導出DLL函數

LIBRARY DLLDemo

EXPORTS

add @ 1

g_nCount DATA

.def文件的規則爲:
  (1)LIBRARY語句說明.def文件相應的DLL
  (2)EXPORTS語句後列出要導出函數的名稱或者變量。可以在.def文件中的導出函數

名後加@n,表示要導出函數的序號爲n,在進行函數調用時,這個序號將發揮其作用,

可通過序號來調用相應的函數(通過序號取得函數方法:GetProcAddress(hDll,

MAKEINTRESOURCE(1))),不過此方法不推薦,因爲難以維護之類的原因,現在基本

上不怎麼使用;
  (3).def文件中的註釋由每個註釋行開始處的分號(;)指定,且註釋不能與語句共享一行。

      (4)導出全局變量格式:全局變量DATADATA是關鍵字,例:g_nCount DATA

其實也可簡單地寫成如下格式:

EXPORTS

add

g_nCount DATA

不需要序號,只給個函數名就OK,記住別把函數名弄錯了。

 

3、導出導入類

   最好不要導出類,類的導出會破壞DLL的通用性,C++寫的類,C#或者其他語言不一定支持,在這裏只是簡單的說一下。

//文件名:point.hpoint類的聲明
#ifndef   POINT_H
#define  POINT_H
#ifdef  DLL_FILE
 class _declspec(dllexport) point //導出類point
#else
 class _declspec(dllimport) point //導入類point
#endif
{
 public:
  float y;
  float x;
  point();
  point(float x_coordinate, float y_coordinate);
};

#endif

此處有宏POINT_HDLL_FILE,第一個宏的作用當然是防止頭文件重編譯,第二個宏則是用來判斷當前是用來導出還是導入的,在此頭的實現文件(.cpp)中先對DLL_FILE定義,則導出此文件,而當用戶用這個動態庫時,因爲不知道DLL_FILE,所以自然沒定義,則會導入此文件。
//文件名:point.cpppoint類的實現
#ifndef DLL_FILE
 #define DLL_FILE
#endif
#include "point.h"
//
point的缺省構造函數
point::point()
{
 x = 0.0;
 y = 0.0;
}

類的導出導入看似乎不適合動態調用,因爲要包含其頭文件,在主調應用程序中生成對象。注意類的導出導入格式如下:

class _declspec(dllexport) 類名 //導出類point
    class _declspec(dllimport) 類名 //導入類point

三、隱式、顯示調用動態庫

1、動態庫隱式調用

   隱式調用也有通過指令與通過IDE設置兩種方式。隱式調用需要.lib文件,把.dll.lib放同一目錄下,然後按以下方法操作

Ø #pragma comment指令:

      #include “..\MyDll.h”----可以是絕對路徑,也以的相對路徑

      #pragma comment(lib, “..\\DllDemo.lib”) ----可以是絕對路徑,也可以是相對路徑

這種方法似乎有點麻煩,必須把路徑設對了,不然可能就找不到。

Ø 在編譯器(VC)中設置:

1、依次選擇tools->options->directories->Show directories for->Library files然後

添加DllDemo.lib的路徑。

2、依次選擇tools->options->directories->Show directories for->Include files然後選擇

MyDll.h的頭文件的路徑。

      3、依次選擇Proctect->Setting->Link然後在Object/library modules中添加上自定義

.lib庫(DllDemo.lib)。

      4、使用自定義庫中的函數的時候#include “MyLib.h”就可以了。

示例代碼:

#include "..\\MyDll.h"

#pragma comment(lib, "..\\Debug\\DLLDemo.lib")

int nResutl = add(1, 2);//直接調用

2、動態庫顯示調用

   顯示調用,不需要頭文件,也不需要.lib庫文件,隨時載入隨時釋放,比較靈活。

查看動態庫信息的工具:

VC++的安裝目錄下的Depends工具,可以看到動態庫的接口信息。

顯式調用:

顯示調用首先得知道Dll的路徑,然後通過API(LoadLibrary)裝載DLL,獲得DLL的句柄,通過API(GetProcAddress)配合LoadLibrary返回的句柄再找到想要調用的函數的地址(返回的是指向對應函數地址的指針),通過函數指針就可以調用函數了。就是這麼簡單,以下是方法:

//定義函數指針類型,用以調用動態庫中函數,以下是定義函數指針類型的格式:

//typedef [返回類型] (__stdcall *[指針名])(形參);

typedef int (* PADD)(int, int); //定義指針類型
int main(int argc, char *argv[])
{  HINSTANCE hDll; //DLL
句柄
  PADD addFun; //函數指針

HINSTANCE hInstance = LoadLibrary(".\\DllDemo.dll");
  if (hInstance != NULL) {     

      addFun = (PADD)GetProcAddress(hInstance, "add");//獲得函數地址

      int nResult = pAdd(1, 6);  //通過函數指針調用函數

      int *pCount = (int *)GetProcAddress(hInstance, "g_nCount");//獲得全局變量指針

      int nCount = *pCount;

FreeLibrary(hInstance );                //記得釋放哦
 }
 return 0;
}

顯示調用與隱式調用卻有較大差異,下面我們來逐一分析。
  首先,語句typedef int ( * lpAddFun)(int,int)定義了一個與add函數接受參數類型和返回值均相同的函數指針類型。隨後,在main函數中定義了lpAddFun的實例addFun

  其次,在函數main中定義了一個DLL HINSTANCE句柄實例hInstance,通過Win32 Api函數LoadLibrary動態加載了DLL模塊並將DLL模塊句柄賦給了hDll

  再次,在函數main中通過Win32 Api函數GetProcAddress得到了所加載DLL模塊中函數add的地址並賦給了addFun。經由函數指針addFun進行了對DLLadd函數的調用;

  最後,應用工程使用完DLL後,在函數main中通過Win32 Api函數FreeLibrary釋放了已經加載的DLL模塊。

      :如果是用.def文件定義的導出函數,併爲函數給定的序號,則可用以下方法調用函數被調用的方法,GetProcAddress(hDll, MAKEINTRESOURCE(1))

 

3.顯示、隱式調用的區別

   從使用層面上講,隱式調用方便,載入一次(主進程啓動的時候載入),就可以到處使用,而顯示調用則麻煩些,得載入釋放,還有取得相應函數的地址之類。然而顯示調用靈活,節省空間,因爲使用時才載入,而不用時釋放,同時顯示調用不需要.lib庫之類。

 

四、動態庫的測試

   動態庫寫好之後,如何測試呢,在這介紹一種比較簡單的方法,在動態庫的工程中,按F5將出現以下界面:

意思是動態庫是不能單獨運行的,你得給它配一個主進程調用它,所以可以寫一個簡單的程序按上面調用動態庫的方法調用動態庫中想要測試的函數,編譯通過後把exe文件的路徑設置在上圖的輸入框中,點確定之後,再按F5,這時主進程就啓動了,你在主進程中觸發調用動態庫函數的操作,這時將跳進動態庫的工程中,動態庫的工程處於調試狀態,你可以一步一步跟蹤,這樣就能很好地測試動態庫,並找出bug

配套源碼下載:http://download.csdn.net/detail/mingojiang/4475172

 

轉載請註明出自:http://blog.csdn.net/mingojiang

 

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