DLL編寫教程

      半年不能上網,最近網絡終於通了,終於能夠更新博客了,寫點什麼呢?決定最近寫一個編程技術系列,其內容是一些通用的編程技術。例如DLL COM Socket ,多線程等等。這些技術的特色就是使用普遍,可是誤解不少;網上教程不少,可是幾乎沒有什麼優質良品。我以近幾個月來的編程經驗發現,頗有必要好好的總結一下這些編程技術了。一來對本身是總結提升,二來能夠方便光顧我博客的朋友。
好了,廢話少說,言歸正傳。第一篇就是《DLL 編寫教程》,爲何起這麼土的名字呢?爲何不叫《輕輕鬆鬆寫DLL 》或者《DLL 一日通》呢?或者更nb 的《深刻簡出DLL 》呢?呵呵,經常上網搜索資料的弟兄天然知道。
本文對通用的DLL 技術作了一個總結,並提供了源代碼打包下載,下載地址爲:
http://www.blogjava.net/Files/wxb_nudt/DLL_SRC.rar
DLL 的優勢 簡單的說,dll 有如下幾個優勢:
1)
節省內存。同一個軟件模塊,如果以源代碼的形式重用,則會被編譯到不一樣的可執行程序中,同時運行這些exe 時這些模塊的二進制碼會被重複加載到內存中。若是使用dll ,則只在內存中加載一次,全部使用該dll 的進程會共享此塊內存(固然,像dll 中的全局變量這種東西是會被每一個進程複製一份的)。
2)
不需編譯的軟件系統升級,若一個軟件系統使用了dll ,則該dll 被改變(函數名不變)時,系統升級只須要更換此dll 便可,不須要從新編譯整個系統。事實上,不少軟件都是以這種方式升級的。例如咱們常常玩的星際、魔獸等遊戲也是這樣進行版本升級的。
3)
Dll 庫能夠供多種編程語言使用,例如用c 編寫的dll 能夠在vb 中調用。這一點上DLL 還作得很不夠,所以在dll 的基礎上發明了COM 技術,更好的解決了一系列問題。
最簡單的dll 開始寫dll 以前,你須要一個c/c++ 編譯器和連接器,並關閉你的IDE 。是的,把你的VC C++ BUILDER 之類的東東都關掉,並打開你以往只用來記電話的記事本程序。不這樣作的話,你可能一生也不明白dll 的真諦。我使用了VC 自帶的cl 編譯器和link 連接器,它們通常都在vc bin 目錄下。(若你沒有在安裝vc 的時候選擇註冊環境變量,那麼就馬上將它們的路徑加入path 吧)若是你仍是由於離開了IDE 而懼怕到哭泣的話,你能夠關閉這個頁面並繼續去看《VC++ 技術內幕》之類無聊的書了。
最簡單的dll 並不比c helloworld 難,只要一個DllMain 函數便可,包含objbase.h 頭文件(支持COM 技術的一個頭文件)。若你以爲這個頭文件名字難記,那麼用windows.H 也能夠。源代碼以下:dll_nolib.cpp
#include <objbase.h>
#include <iostream.h>
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       cout<<'Dll is attached!'<<endl;
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
       cout<<'Dll is detached!'<<endl;
       g_hModule=NULL;
       break;
    }
    return true;
}

其中DllMain 是每一個dll 的入口函數,如同c main 函數同樣。DllMain 帶有三個參數,hModule 表示本dll 的實例句柄(聽不懂就不理它,寫過windows 程序的天然懂),dwReason 表示dll 當前所處的狀態,例如DLL_PROCESS_ATTACH 表示dll 剛剛被加載到一個進程中,DLL_PROCESS_DETACH 表示dll 剛剛從一個進程中卸載。固然還有表示加載到線程中和從線程中卸載的狀態,這裏省略。最後一個參數是一個保留參數(目前和dll 的一些狀態相關,可是不多使用)。
從上面的程序能夠看出,當dll 被加載到一個進程中時,dll 打印'Dll is attached!' 語句;當dll 從進程中卸載時,打印'Dll is detached!' 語句。
編譯dll 須要如下兩條命令:
cl /c dll_nolib.cpp

這條命令會將cpp 編譯爲obj 文件,若不使用/c 參數則cl 還會試圖繼續將obj 連接爲exe ,可是這裏是一個dll ,沒有main 函數,所以會報錯。沒關係,繼續使用連接命令。
Link /dll dll_nolib.obj

這條命令會生成dll_nolib.dll
注意,由於編譯命令比較簡單,因此本文不討論nmake ,有興趣的可使用nmake ,或者寫個bat 批處理來編譯連接dll
加載DLL (顯式調用) 使用dll 大致上有兩種方式,顯式調用和隱式調用。這裏首先介紹顯式調用。編寫一個客戶端程序:dll_nolib_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
    // 加載咱們的dll
    HINSTANCE hinst=:: LoadLibrary('dll_nolib.dll');
    if (NULL != hinst)
    {
       cout<<'dll loaded!'<<endl;
    }
    return 0;
}

注意,調用dll 使用LoadLibrary 函數,它的參數就是dll 的路徑和名稱,返回值是dll 的句柄。 使用以下命令編譯連接客戶端:
Cl dll_nolib_client.cpp

並執行dll_nolib_client.exe ,獲得以下結果:
Dll is attached!
dll loaded!
Dll is detached!

以上結果代表dll 已經被客戶端加載過。可是這樣僅僅可以將dll 加載到內存,不能找到dll 中的函數。
使用dumpbin 命令查看DLL 中的函數Dumpbin 命令能夠查看一個dll 中的輸出函數符號名,鍵入以下命令:
Dumpbin –exports dll_nolib.dll

經過查看,發現dll_nolib.dll 並無輸出任何函數。
如何在dll 中定義輸出函數 整體來講有兩種方法,一種是添加一個def 定義文件,在此文件中定義dll 中要輸出的函數;第二種是在源代碼中待輸出的函數前加上__declspec(dllexport) 關鍵字。
Def 文件 首先寫一個帶有輸出函數的dll ,源代碼以下:dll_def.cpp
#include <objbase.h>
#include <iostream.h>
void FuncInDll (void)
{
    cout<<'FuncInDll is called!'<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
        g_hModule=NULL;
        break;
    }
    return TRUE;
}

這個dll def 文件以下:dll_def.def
;
; dll_def module-definition file
;
LIBRARY         dll_def.dll
DESCRIPTION     '(c)2007-2009 Wang Xuebin'
EXPORTS
                FuncInDll @1 PRIVATE

你會發現def 的語法很簡單,首先是LIBRARY 關鍵字,指定dll 的名字;而後一個可選的關鍵字DESCRIPTION ,後面寫上版權等信息(不寫也能夠);最後是EXPORTS 關鍵字,後面寫上dll 中全部要輸出的函數名或變量名,而後接上@ 以及依次編號的數字(從1 N ),最後接上修飾符。
用以下命令編譯連接帶有def 文件的dll
Cl /c dll_def.cpp
Link /dll dll_def.obj /def:dll_def.def

再調用dumpbin 查看生成的dll_def.dll
Dumpbin –exports dll_def.dll

獲得以下結果:
Dump of file dll_def.dll
File Type: DLL
Section contains the following exports for dll_def.dll
           0 characteristics
    46E4EE98 time date stamp Mon Sep 10 15:13:28 2007
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names
    ordinal hint RVA      name
          1    0 00001000 FuncInDll
Summary
        2000 .data
        1000 .rdata
        1000 .reloc
        6000 .text

觀察這一行
          1    0 00001000 FuncInDll

會發現該dll 輸出了函數FuncInDll
顯式調用DLL 中的函數 寫一個dll_def.dll 的客戶端程序:dll_def_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
    // 定義一個函數指針
    typedef void (* DLLWITHLIB )(void);
    // 定義一個函數指針變量
    DLLWITHLIB pfFuncInDll = NULL;
    // 加載咱們的dll
    HINSTANCE hinst=: :LoadLibrary('dll_def.dll');
    if (NULL != hinst)
    {
       cout<<'dll loaded!'<<endl;
    }
    // 找到dll FuncInDll 函數
    pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, 'FuncInDll');
    // 調用dll 裏的函數
    if (NULL != pfFuncInDll)
    {
       (*pfFuncInDll)();   
    }
    return 0;
}

有兩個地方值得注意,第一是函數指針的定義和使用,不懂的隨便找本c++ 書看看;第二是GetProcAddress 的使用,這個API 是用來查找dll 中的函數地址的,第一個參數是DLL 的句柄,即LoadLibrary 返回的句柄,第二個參數是dll 中的函數名稱,即dumpbin 中輸出的函數名(注意,這裏的函數名稱指的是編譯後的函數名,不必定等於dll 源代碼中的函數名)。
編譯連接這個客戶端程序,並執行會獲得:
dll loaded!
FuncInDll is called!

這代表客戶端成功調用了dll 中的函數FuncInDll
__declspec(dllexport) 爲每一個dll def 顯得很繁雜,目前def 使用已經比較少了,更多的是使用__declspec(dllexport) 在源代碼中定義dll 的輸出函數。
Dll 寫法同上,去掉def 文件,並在每一個要輸出的函數前面加上聲明__declspec(dllexport) ,例如:
__declspec(dllexport) void FuncInDll (void)

這裏提供一個dll 源程序dll_withlib.cpp ,而後編譯連接。連接時不須要指定/DEF: 參數,直接加/DLL 參數便可,
Cl /c dll_withlib.cpp
Link /dll dll_withlib.obj

而後使用dumpbin 命令查看,獲得:
1    0 00001000 ?FuncInDll@@YAXXZ

可知編譯後的函數名爲?FuncInDll@@YAXXZ ,而並非FuncInDll ,這是由於c++ 編譯器基於函數重載的考慮,會更改函數名,這樣使用顯式調用的時候,也必須使用這個更改後的函數名,這顯然給客戶帶來麻煩。爲了不這種現象,可使用extern 「C」 指令來命令c++ 編譯器以c 編譯器的方式來命名該函數。修改後的函數聲明爲:
extern 'C' __declspec(dllexport) void FuncInDll (void)

dumpbin 命令結果:
1    0 00001000 FuncInDll

這樣,顯式調用時只需查找函數名爲FuncInDll 的函數便可成功。
extern 「C」 使用extern 「C」 關鍵字實際上至關於一個編譯器的開關,它能夠將c++ 語言的函數編譯爲c 語言的函數名稱。即保持編譯後的函數符號名等於源代碼中的函數名稱。
隱式調用DLL 顯式調用顯得很是複雜,每次都要LoadLibrary ,而且每一個函數都必須使用GetProcAddress 來獲得函數指針,這對於大量使用dll 函數的客戶是一種困擾。而隱式調用可以像使用c 函數庫同樣使用dll 中的函數,很是方便快捷。
下面是一個隱式調用的例子:dll 包含兩個文件dll_withlibAndH.cpp dll_withlibAndH.h
代碼以下:dll_withlibAndH.h
extern 'C' __declspec(dllexport) void FuncInDll (void);

dll_withlibAndH.cpp
#include <objbase.h>
#include <iostream.h>
#include 'dll_withLibAndH.h'// 看到沒有,這就是咱們增長的頭文件
extern 'C' __declspec(dllexport) void FuncInDll (void)
{
    cout<<'FuncInDll is called!'<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
        g_hModule=NULL;
        break;
    }
    return TRUE;
}

編譯連接命令:
Cl /c dll_withlibAndH.cpp
Link /dll dll_withlibAndH.obj

在進行隱式調用的時候須要在客戶端引入頭文件,並在連接時指明dll 對應的lib 文件(dll 只要有函數輸出,則連接的時候會產生一個與dll 同名的lib 文件)位置和名稱。而後如同調用api 函數庫中的函數同樣調用dll 中的函數,不須要顯式的LoadLibrary GetProcAddress 。使用最爲方便。客戶端代碼以下:dll_withlibAndH_client.cpp
#include 'dll_withLibAndH.h'
// 注意路徑,加載 dll 的另外一種方法是 Project | setting | link 設置裏
#pragma comment(lib,'dll_withLibAndH.lib')
int main(void)
{
    FuncInDll();// 只要這樣咱們就能夠調用dll 裏的函數了
    return 0;
}

__declspec(dllexport) __declspec(dllimport) 配對使用 上面一種隱式調用的方法很不錯,可是在調用DLL 中的對象和重載函數時會出現問題。由於使用extern 「C」 修飾了輸出函數,所以重載函數確定是會出問題的,由於它們都將被編譯爲同一個輸出符號串(c 語言是不支持重載的)。
事實上不使用extern 「C」 是可行的,這時函數會被編譯爲c++ 符號串,例如(?FuncInDll@@YAXH@Z ?FuncInDll@@YAXXZ ),當客戶端也是c++ 時,也能正確的隱式調用。
這時要考慮一個狀況:若DLL1.CPP 是源,DLL2.CPP 使用了DLL1 中的函數,但同時DLL2 也是一個DLL ,也要輸出一些函數供Client.CPP 使用。那麼在DLL2 中如何聲明全部的函數,其中包含了從DLL1 中引入的函數,還包括本身要輸出的函數。這個時候就須要同時使用__declspec(dllexport) __declspec(dllimport) 了。前者用來修飾本dll 中的輸出函數,後者用來修飾從其它dll 中引入的函數。
全部的源代碼包括DLL1.H DLL1.CPP DLL2.H DLL2.CPP Client.cpp 。源代碼能夠在下載的包中找到。你能夠編譯連接並運行試試。
值得關注的是DLL1 DLL2 中都使用的一個編碼方法,見DLL2.H
#ifdef DLL_DLL2_EXPORTS
#define DLL_DLL2_API __declspec(dllexport)
#else
#define DLL_DLL2_API __declspec(dllimport)
#endif
DLL_DLL2_API void FuncInDll2(void);
DLL_DLL2_API void FuncInDll2(int);

在頭文件中以這種方式定義宏DLL_DLL2_EXPORTS DLL_DLL2_API ,能夠確保DLL 端的函數用__declspec(dllexport) 修飾,而客戶端的函數用__declspec(dllimport) 修飾。固然,記得在編譯dll 時加上參數/D 「DLL_DLL2_EXPORTS」 ,或者乾脆就在dll cpp 文件第一行加上#define DLL_DLL2_EXPORTS
VC 生成的代碼也是這樣的!事實證實,我是抄襲它的,hoho
DLL 中的全局變量和對象 解決了重載函數的問題,那麼dll 中的全局變量和對象都不是問題了,只是有一點語法須要注意。如源代碼所示:dll_object.h
#ifdef DLL_OBJECT_EXPORTS
#define DLL_OBJECT_API __declspec(dllexport)
#else
#define DLL_OBJECT_API __declspec(dllimport)
#endif
DLL_OBJECT_API void FuncInDll(void);
extern DLL_OBJECT_API int g_nDll;
class DLL_OBJECT_API CDll_Object {
public:
    CDll_Object(void);
    show(void);
    // TODO: add your methods here.
};

Cpp 文件dll_object.cpp 以下:
#define DLL_OBJECT_EXPORTS
#include <objbase.h>
#include <iostream.h>
#include 'dll_object.h'
DLL_OBJECT_API void FuncInDll(void)
{
    cout<<'FuncInDll is called!'<<endl;
}
DLL_OBJECT_API int g_nDll = 9;
CDll_Object::CDll_Object()
{
    cout<<'ctor of CDll_Object'<<endl;
}
CDll_Object::show()
{
    cout<<'function show in class CDll_Object'<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
        g_hModule=NULL;
        break;
    }
    return TRUE;
}

編譯連接完後Dumpbin 一下,能夠看到輸出了5 個符號:
1    0 00001040 ??0CDll_Object@@QAE@XZ
2    1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z
3    2 00001020 ?FuncInDll@@YAXXZ
4    3 00008040 ?g_nDll@@3HA
5    4 00001069 ?show@CDll_Object@@QAEHXZ

它們分別表明類CDll_Object ,類的構造函數,FuncInDll 函數,全局變量g_nDll 和類的成員函數show 。下面是客戶端代碼:dll_object_client.cpp
#include 'dll_object.h'
#include <iostream.h>
// 注意路徑,加載 dll 的另外一種方法是 Project | setting | link 設置裏
#pragma comment(lib,'dll_object.lib')
int main(void)
{
    cout<<'call dll'<<endl;
    cout<<'call function in dll'<<endl;
    FuncInDll();// 只要這樣咱們就能夠調用dll 裏的函數了
    cout<<'global var in dll g_nDll ='<<g_nDll<<endl;
    cout<<'call member function of class CDll_Object in dll'<<endl;
    CDll_Object obj;
    obj.show();
    return 0;
}

運行這個客戶端能夠看到:
call dll
call function in dll
FuncInDll is called!
global var in dll g_nDll =9
call member function of class CDll_Object in dll
ctor of CDll_Object
function show in class CDll_Object

可知,在客戶端成功的訪問了dll 中的全局變量,並建立了dll 中定義的C++ 對象,還調用了該對象的成員函數。
中間的小結 牢記一點,說到底,DLL 是對應C 語言的動態連接技術,在輸出C 函數和變量時顯得方便快捷;而在輸出C++ 類、函數時須要經過各類手段,並且也並無完美的解決方案,除非客戶端也是c++
記住,只有COM 是對應C++ 語言的技術。
下面開始對各各問題一一小結。
顯式調用和隱式調用 什麼時候使用顯式調用?什麼時候使用隱式調用?我認爲,只有一個時候使用顯式調用是合理的,就是當客戶端不是C/C++ 的時候。這時是沒法隱式調用的。例如用VB 調用C++ 寫的dll 。(VB 我不會,因此沒有例子)
Def __declspec(dllexport) 其實def 的功能至關於extern 「C」 __declspec(dllexport) ,因此它也僅能處理C 函數,而不能處理重載函數。而__declspec(dllexport) __declspec(dllimport) 配合使用可以適應任何狀況,所以__declspec(dllexport) 是更爲先進的方法。因此,目前廣泛的見解是不使用def 文件,我也贊成這個見解。
從其它語言調用DLL 從其它編程語言中調用DLL ,有兩個最大的問題,第一個就是函數符號的問題,前面已經屢次提過了。這裏有個兩難選擇,若使用extern 「C」 ,則函數名稱保持不變,調用較方便,可是不支持函數重載等一系列c++ 功能;若不使用extern 「C」 ,則調用前要查看編譯後的符號,很是不方便。
第二個問題就是函數調用壓棧順序的問題,即__cdecl __stdcall 的問題。__cdecl 是常規的C/C++ 調用約定,這種調用約定下,函數調用後棧的清理工做是由調用者完成的。__stdcall 是標準的調用約定,即這些函數將在返回到調用者以前將參數從棧中刪除。
這兩個問題DLL 都不能很好的解決,只能說湊合着用。可是在COM 中,都獲得了完美的解決。因此,要在Windows 平臺實現語言無關性,仍是隻有使用COM 中間件。
總而言之,除非客戶端也使用C++ ,不然dll 是不便於支持函數重載、類等c++ 特性的。DLL c 函數的支持很好,我想這也是爲何windows 的函數庫使用C dll 實現的理由之一。
VC 中編寫DLL VC 中建立、編譯、連接dll 是很是方便的,點擊file àNew àProject àWin32 Dynamic-Link Library ,輸入dll 名稱dll_InVC 而後點擊肯定。而後選擇A DLL that export some symbols ,點擊Finish 。便可獲得一個完整的DLL
仔細觀察其源代碼,是否是有不少地方似曾相識啊,哈哈! [http://www.armjishu.com/bbs/viewtopic.php?id=1046]
相關文章
相關標籤/搜索