1. 程式人生 > >Visual Studio 2010 中的DLL基礎知識

Visual Studio 2010 中的DLL基礎知識

對於DLL的連結,新手還是會有很大麻煩,這裡把一些基礎知識列出來,本文所有內容源於MSDN文件

從DLL中匯出函式

DLL和一般可執行程式的區別:

  主要是DLL只能在系統中執行一個例項並且它無法擁有一些一般程式有的事物,比如棧,全域性記憶體,檔案控制代碼和訊息佇列等等。同時DLL檔案包含一個匯出表(Exports table),這個匯出表包含了dll匯出給其他程式用的所有函式。它們是這個DLL的進入點。也只有在這個匯出表上的函式才可以被其他程式執行。

  P.S. 可以用DUMPBIN工具來檢視dll的匯出表。

從DLL匯出函式有兩種方法:

  1. 建立一個模組定義檔案.def,這種情況適用於當你希望使用序號來匯出函式時
  2. 在函式定義中使用__declspec(dllexport)關鍵字

dllexport和dllimport

dllexport 和dllimport是微軟自己的C++識別符號,我們可以用它來匯入和匯出函式,資料以及物件,用法如下:

__declspec( dllimport ) declarator
__declspec( dllexport ) declarator

使用dllexport宣告的函式無需再使用.def檔案。

dll介面指的是dll中所有已知的被匯出的函式和資料的引用。 即所有被宣告為dllimport或者dllexport的東西。但是dllexport和dllimport是有區別的。

對於定義,只能被宣告為dllexport。比如

__declspec( dllimport ) int func() {   // Error; dllimport
                                       // prohibited on definition.
   return 1;
}
__declspec( dllimport ) int i = 10;  // Error; this is a
                                     // definition.

這裡具有語義錯誤,因為dllimport只能用於匯入宣告而不能用於匯入定義

__declspec( dllexport ) int i = 10;     // Okay--export definition

但是dllexport則可以匯出定義。也就是所,dllexport表示是一個定義而dllimport表示是一個宣告。如果需要使用dllexport來表示宣告,就必須在前面加一個extern

#define DllImport   __declspec( dllimport )
#define DllExport   __declspec( dllexport )

extern DllImport int k; // These are both correct and imply a
DllImport int j;        // declaration.

MSDN上的這個文件給出了一些更復雜的例子,由於裡面有一些隱含的意義所以剛看的時候很容易搞混了,我註釋一下:

static __declspec( dllimport ) int l; // Error; not declared extern.

                                      // 錯誤的原因是因為加上了static 以後int l;實際上是一個定義而非宣告,它被預設初始化為0了
void func() {
    static __declspec( dllimport ) int s;  // Error; not declared
                                           // extern.

                                           // 錯誤原因同上

    __declspec( dllimport ) int m;         // Okay; this is a 
                                           // declaration.

    __declspec( dllexport ) int n;         // Error; implies external
                                           // definition in local scope.

                                           // 錯誤的原因是這個在一個函式的作用域內嘗試匯出一個定義
    extern __declspec( dllimport ) int i;  // Okay; this is a
                                           // declaration.
                                           // dllimport加不加extern都是表示宣告

    extern __declspec( dllexport ) int k;  // Okay; extern implies
                                           // declaration.
    __declspec( dllexport ) int x = 5;     // Error; implies external
                                           // definition in local scope.
}


網上搜了一下,似乎對dllimport和dllexport的討論都滿是困惑,其實,即使翻了msdn的文件也不一定就看得懂。從他們提供的程式碼裡我們可以清楚的看出這兩個關鍵字的用法的區別:
這個是一個dll檔案中準備要匯出的函式Test

// lib_link_input_1.cpp
// compile with: /LD
__declspec(dllexport) int Test() {
   return 213;
}


這個是使用那個函式的程式:

// lib_link_input_2.cpp
// compile with: /EHsc lib_link_input_1.lib
__declspec(dllimport) int Test();
#include <iostream>
int main() {
   std::cout << Test() << std::endl;
}


所以,由此可見,其實dllimport和dllexport的區別就是,後者是dll中要匯出給別人用的東西所需要的修飾符。而前者則是要從別的dll那裡匯入東西來用的時候用的修飾符。

此外,值得注意的是dllimport對於函式宣告是可選的,不過顯式的使用它能加速編譯,程式碼的可讀性也更好,具體的細節在這篇文件裡有說明。而對於dll的公共資料和物件則必須顯式的指明dllimport。這裡有一個很好的技巧來使得dll和客戶程式可以使用同一個標頭檔案:

#ifdef _EXPORTING
   #define CLASS_DECLSPEC    __declspec(dllexport)
#else
   #define CLASS_DECLSPEC    __declspec(dllimport)
#endif

class CLASS_DECLSPEC CExampleA : public CObject
{ ... class definition ... };

通過定義這個_EXPORTING預處理符號,使得編譯器可以明白現在是在編譯dll還是匯入定義

在類中使用dllimport和dllexport

要對類使用這兩個屬性的話有幾種做法,第一種是直接dllexport整個類:

#define DllExport   __declspec( dllexport )

class DllExport C {
   int i;
   virtual int func( void ) { return 1; }
};

此時,類的所有成員函式和靜態資料都將被匯出,在這種情況下是不可以再對類成員顯式的定義dllexport或者dllimport的。因此,除了純虛擬函式外其他的所有成員都必須提供定義。(除了純虛解構函式,因為它總是被基類的解構函式呼叫,所以必須提供一個定義)

使用dllimport來匯入一個類宣告的話,它的所有成員函式和靜態成員都被匯入。

可匯出的類的繼承問題

所有的可匯出類的基類應該都要是可匯出的,否則的話,編譯器會產生一個警告(不是錯誤)。此外,這個類的所有可訪問的成員也都必須是可匯出的,這樣才可以允許你dllexport一個你自己實現的dllimport的類的子類

連結方式的選擇

可執行檔案連結DLL的方式有兩種:

  • Implicity linking (隱式)
  • Explicit linking (顯式)

對於隱式連結,程式使用DLL連結到DLL建立者提供的一個用於匯入的庫(.lib)檔案。系統在應用程式使用這個DLL的時候載入它。這種情況下應用程式呼叫這個DLL的匯出的函式就好像是在呼叫程式內部的函式一樣

對於顯式連結,程式好似用DLL必須先顯式的載入和解除安裝DLL並且訪問DLL的匯出的函式。因此應用程式只能通過函式指標來呼叫DLL中匯出的函式。

對於任何DLL兩種呼叫都可以使用。大部分程式都會使用隱式連結,因為這種方式最容易使用。下面開始先說明一下如何選擇兩種連結方式

隱式連結

DLL的建立者必須提供一個.LIB檔案來作為匯入庫提供給應用程式的進行外部引用。匯入庫僅包含載入DLL的程式碼和實現DLL函式呼叫的程式碼,因此程式在匯入庫中找到外部函式後就會通知聯結器說:這個函式的程式碼在DLL中,去解析對DLL的外部引用。而連結器就會在可執行檔案中新增資訊通知系統在程序啟動時到什麼地方去找DLL程式碼。系統找到DLL以後就會把DLL模組對映到程序的地址空間去。

如果這個DLL有初始化程式碼的入口點函式的話,作業系統還會先呼叫這個函式。系統還會修改程序的可執行程式碼以提供DLL函式的其實地址。

顯式連結

顯式連結的兩個主要缺點是:

  • 如果DLL具有DLLMain這個入口點函式,作業系統在呼叫LoadLibrary的執行緒中會呼叫此函式,但是如果之後沒有呼叫FreeLibrary,那麼下次有其他程式再呼叫LoadLibrary的時候就不會呼叫這個入口點函數了。所以,如果DLL使用入口點函式來為程序的每個執行緒初始化的話,顯式連結就會出問題。
  • 如果DLL將靜態作用域資料宣告為__declspec(thread)的話,顯式連結會導致保護錯誤。因此建立DLL時必須避免使用執行緒本地儲存區。

所以一般在下面這些情況的時候才使用顯式連結

  • 知道執行時才知道DLL的名稱
  • 啟動時未找到DLL的話系統會終止使用隱式連結的程序。但是不會終止顯式的。
  • 隱式連結的DLL中如果有失敗的DllMain函式的話程序也會被幹掉。同樣顯式的不會
  • 程式在執行時會載入所有隱式的DLL,因此會導致程式啟動很慢。

DLL的查詢順序

Windows查詢DLL的順序如下:

  1. 程序執行目錄
  2. 當前目錄
  3. Windows system 目錄,GetSystemDirectory函式返回此目錄
  4. Windows 目錄,GetWindowsDirectory函式返回此目錄
  5. PATH環境變數中列出的目錄

特別注意的是LIBPATH環境變數是沒用的

隱式連結的步驟

要進行隱式連結需要得到這3個東西

  1. 一個包含了匯出函式的宣告或者C++類的標頭檔案,所有的類,函式,資料必須標識為__declspec(dllimport)
  2. 一個用來連結的.lib檔案(連結器在DLL編譯的時候會生成這個匯入庫)
  3. 實際的dll檔案

具體在VS2010的操作中只需要在下面這個設定中填入lib檔案的地址即可:

顯式連結的步驟

複雜的情況