動態連結庫DLL_第1篇

QingLiXueShi發表於2016-01-17

動態連結庫通常不能直接執行,也不能接收訊息。它們是一些獨立的檔案,其中包含能被可執行程式或其他DLL呼叫來完成某項工作的函式。只有在其他模組呼叫動態連結庫中的函式時,它才發揮作用。實際程式設計時,可把完成某種功能的函式放在一個動態連結庫中,提供給其他程式呼叫。

Windows API中所有函式都包含在DLL中,比較重要的有3個,分別為:

1、Kernel32.dll

包含用於管理記憶體、程式和執行緒的函式。

2、User32.dll

包含用於執行使用者介面任務(如視窗建立和訊息傳遞的函式等)的函式。

3、GDI32.dll

包含用於畫圖和顯示文字的函式。

 

靜態庫和動態庫

1、靜態庫

函式和資料被編譯成一個二進位制檔案,副檔名為.LIB。使用靜態庫時,編譯連結可執行檔案時,連結器從庫中複製這些函式和資料並把它們和應用程式的其他模組組合起來建立最終的可執行檔案。釋出產品時,只需要釋出這個可執行檔案,並不需要釋出被使用的靜態庫。

2、動態庫

需要提供2個檔案:引入庫.lib檔案和DLL檔案。動態庫的引入庫.lib檔案與靜態庫雖然字尾相同,但含義完全不同。引入庫檔案.lib包含DLL匯出的函式和變數的符號名,.dll檔案包含DLL實際的函式和資料。編譯連結時,只需要連結DLL的引入庫檔案,DLL中的函式程式碼和資料並不複製到可執行檔案中,直到可執行程式執行時,才去載入需要的DLL,將該DLL對映到程式的地址空間,然後訪問DLL中的匯出函式。釋出產品時,除了釋出可執行檔案,還要同時釋出動態連結庫。

 

如果多個應用程式使用同一個DLL,該DLL的頁面只需要放入記憶體一次,所有的應用程式都可以共享它的頁面。

有兩種載入動態連結庫的方式:隱式連結和顯式載入。

應用程式如果想訪問某個DLL中的函式,該函式必須是已經被匯出的函式。為了讓DLL匯出一些函式,需要在每一個將要被匯出的函式前面新增標誌符_declspec(dllexport)。

客戶呼叫DLL的匯出函式時,必須先申明該函式是外部的extern。除了使用extern關鍵字表明函式是外部定義的之外,還可以使用_declspec(dllimport)來表明函式是從動態連結庫中引入的。使用_declspec(dllimport)宣告外部函式,能夠明確告訴編譯器函式是從動態連結庫中引入的,編譯器可以生成執行效率更高的程式碼。

通常在編寫動態連結庫時,都會提供一個標頭檔案,在此檔案中提供DLL匯出函式原型的宣告,以及函式的有關注釋文件。程式編譯時,標頭檔案不參與編譯,原始檔單獨編譯。

dll_1.h檔案

#ifdef DLL1_API
#else 
#define DLL1_API _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

dll_1.cpp檔案

#define DLL1_API _declspec(dllexport)
#include "dll_1.h"

DLL1_API int add(int a, int b)
{
    return a + b;
}

DLL1_API int subtract(int a, int b)
{
    return a - b;
}

 

動態連結庫還可以匯出C++類。

dll_1.h檔案

#ifdef DLL1_API
#else 
#define DLL1_API _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

class DLL1_API point {
public:
    void Output(int x, int y);
};

dll_1.cpp檔案

#define DLL1_API _declspec(dllexport)
#include "dll_1.h"

DLL1_API int add(int a, int b)
{
    return a + b;
}

DLL1_API int subtract(int a, int b)
{
    return a - b;
}

void point::Output(int x, int y)
{
    HWND hwnd = GetForegroundWindow();
    HDC hdc = GetDC(hwnd);
    TCHAR buf[20] = { '\0' };
    swprintf(buf, 20, _T("x = %d, y = %d"), x, y);
    TextOut(hdc, 0, 0, buf, wcslen(buf));
    ReleaseDC(hwnd, hdc);
}

如果隱式連結載入DLL,.dll更新後,需要將新的.dll和.lib檔案複製到相應工程目錄。

動態連結庫還可以不匯出整個C++類,而只匯出C++類的某些函式。

dll_1.h檔案

class/* DLL1_API*/ point {
public:
    void DLL1_API Output(int x, int y);
    void test();
};

如果宣告類時,指定了匯出標誌,該類中的所有函式都將被匯出,否則只有那些宣告時指定了匯出標誌的類成員函式才被匯出。

 

名字改編問題

C++編譯器生成DLL時,會對匯出函式的名字進行改編,不同編譯器使用的改編規則不同,改編後的名字是不一樣的。如果利用不同的編譯器分別生成DLL和訪問DLL的客戶端程式,客戶端程式訪問DLL的匯出函式時就會出現問題。因此,希望動態連結庫檔案編譯時,匯出函式的名稱不要發生改變。做法是,定義匯出函式時,加上限定符extern “C”,字母C大寫。

dll_1.h檔案

#ifdef DLL1_API
#else 
#define DLL1_API extern "C" _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

dll_1.cpp檔案

#define DLL1_API extern "C" _declspec(dllexport)
#include "dll_1.h"

DLL1_API int add(int a, int b)
{
    return a + b;
}

DLL1_API int subtract(int a, int b)
{
    return a - b;
}

extern “C”可以解決C++和C語言之間相互呼叫時函式命名的問題,但該方法不能用於匯出一個類的成員函式,只能用於匯出全域性函式。如果匯出函式的呼叫約定發生改變,即使使用限定符extern “C”,函式的名字仍會發生改變。

 

顯示載入方式載入DLL

LoadLibrary的作用是將指定的可執行模組對映到呼叫程式的地址空間,返回值是載入模組的控制程式碼。GetProcAddress函式用來獲得匯出函式的地址。動態載入DLL時,客戶端不再需要包含匯出函式宣告的標頭檔案和引入庫檔案,只需要.dll檔案即可。

void CMFCApplication1Dlg::OnBnClickedBtnSub()
{
    HMODULE HInst = LoadLibrary(_T("ConsoleApplication1.dll"));
    if (HInst == NULL)
        return;
    typedef int (*ADDPROC)(int a, int b);

    ADDPROC Add = (ADDPROC)GetProcAddress(HInst, "add");
    if (!Add) {
        MessageBox(_T("獲取函式地址失敗!"));
        return;
    }
    CString str;
    str.Format(_T("5 + 3 = %d."), Add(5, 3));
    MessageBox(str);
}

實際上採用隱式連結方式訪問DLL時,程式啟動時也是通過呼叫LoadLibrary函式載入該程式的動態連結庫的。

如果採用動態載入方式使用DLL,訪問DLL的客戶端程式,如果對DLL的訪問已經完成,不再需要訪問該DLL時,應呼叫FreeLibrary函式釋放該DLL。FreeLibrary主要減少被載入的DLL的引用計數。當計數變為0時,該DLL模組將從呼叫程式的地址空間解除安裝。

 

DllMain函式

DLL的入口函式是DllMain,該函式是可選的。編寫DLL程式時,可以提供也可以不提供DllMain函式。如果提供了DllMain函式,系統載入該DLL時,就會呼叫該函式。

不應該在DllMain函式中進行復雜的呼叫,因為載入該動態連結庫時,可能還有一些核心動態連結庫沒有被載入。如果自己編寫的DllMain函式需要呼叫核心動態連結庫中的某些函式,就會導致程式終止。

 

參考資料:

1、《VC++深入詳解》

相關文章