溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

動態鏈接庫DLL的編寫

發布時間:2020-06-18 23:13:04 來源:網絡 閱讀:1178 作者:darhx 欄目:移動開發

看過不少DLL編程方面的書,但是實際工作中還沒有編寫過,對DLL的編寫一直處于一知半解的狀態。趁著這兩天有空,趕緊發篇博文總結總結!


如果各位擅長使用命令行來進行編譯、鏈接,那么可以看一下這篇博文(轉載)。

http://www.blogjava.net/wxb_nudt/archive/2007/09/11/144371.html


源代碼下載地址(鏈接來自原博文).

http://www.blogjava.net/Files/wxb_nudt/DLL_SRC.rar

如果各位跟我一樣,還是只會弱弱的使用IDE,那么就看接下去的內容吧,大體上和上述的博文內容一致,只是使用命令行的部分,我改成了使用VS2010。


開頭是用來感謝原作者的!


最簡單的dll

首先用VS2010創建一個空項目。

動態鏈接庫DLL的編寫

最簡單的dll并不比chelloworld難,只要一個DllMain函數即可,包含objbase.h頭文件(支持COM技術的一個頭文件)。若你覺得這個頭文件名字難記,那么用windows.H也可以。向工程里面添加一個文件main.cpp,內容如下:

#include <objbase.h>
#include <iostream>
using namespace std;
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;
}

然后選擇生成解決方案,是不是報了這個錯誤fatal error LNK1561: 必須定義入口點?

這是因為IDE默認是生成exe文件,這個文件中沒有main函數,當然就報錯了。

進行如下的設置即可。

項目->屬性->配置屬性->常規->項目默認值->配置類型 選擇動態庫(.dll)。

再次生成解決方案,這次就成功了吧?去工程目錄下查找,dll已經在了。



其中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(顯式調用)

使用dll大體上有兩種方式,顯式調用和隱式調用。這里首先介紹顯式調用。依舊創建一個空工程,加入文件main.cpp,內容如下:


#include <windows.h>
#include <iostream>
using namespace std;
int main(void)
{
    //加載我們的dll
    HINSTANCE hinst=::LoadLibrary("dll_nolib.dll");
    if (NULL != hinst)
    {
        cout<<"dll loaded!"<<endl;
    }
    return 0;
}

注意,調用dll使用LoadLibrary函數,它的參數就是dll的路徑和名稱,返回值是dll的句柄。

把之前生成的dll放到運行目錄下,直接編譯運行程序,即可得到如下結果:

Dll is attached!

dll loaded!

Dll is detached!


以上結果表明dll已經被客戶端加載過。但是這樣僅僅能夠將dll加載到內存,不能找到dll中的函數。


網上有很多dll函數查看器可以下載,任意下載一個。用dll函數查看器打開之前生成的dll,可以發現目前的dll里面并沒有任何函數。


如何在dll中定義輸出函數

總體來說有兩種方法,一種是添加一個def定義文件,在此文件中定義dll中要輸出的函數;第二種是在源代碼中待輸出的函數前加上__declspec(dllexport)關鍵字。


Def文件

首先寫一個帶有輸出函數的dll,源代碼main.cpp如下:

#include <objbase.h>
#include <iostream>
using namespace std;
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


LIBRARY         dll_def.dll
EXPORTS
                FuncInDll @1 PRIVATE

你會發現def的語法很簡單,首先是LIBRARY關鍵字,指定dll的名字;然后是EXPORTS關鍵字,后面寫上dll中所有要輸出的函數名或變量名,然后接上@以及依次編號的數字(從1到N),最后接上修飾符(可選)。


在模塊定義文件中配置def文件的名稱

動態鏈接庫DLL的編寫


接下來生成dll文件。用dll函數查看器打開dll文件,可以發現已經有了一個導出函數。

動態鏈接庫DLL的編寫


顯式調用DLL中的函數

寫一個dll_def.dll的客戶端程序:dll_def_client


#include <windows.h>
#include <iostream>
using namespace std;
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中的函數名稱,即dll函數查看器中看到的函數名(注意,這里的函數名稱指的是編譯后的函數名,不一定等于dll源代碼中的函數名)。

編譯,鏈接,運行后可以看到:

動態鏈接庫DLL的編寫

這表明客戶端成功調用了dll中的函數FuncInDll。


__declspec(dllexport)

為每個dll寫def顯得很繁雜,目前def使用已經比較少了,更多的是使用__declspec(dllexport)在源代碼中定義dll的輸出函數。

Dll寫法同上,去掉def文件,并在每個要輸出的函數前面加上聲明__declspec(dllexport),例如:

__declspec(dllexport) void FuncInDll (void)

重新生成dll文件,并用查看器查看。

動態鏈接庫DLL的編寫

可知編譯后的函數名為?FuncInDll@@YAXXZ,而并不是FuncInDll,這是因為c++編譯器基于函數重載的考慮,會更改函數名,這樣使用顯式調用的時候,也必須使用這個更改后的函數名,這顯然給客戶帶來麻煩。為了避免這種現象,可以使用extern “C”指令來命令c++編譯器以c編譯器的方式來命名該函數。修改后的函數聲明為:

extern "C" __declspec(dllexport) void FuncInDll (void)

重新生成一下,可以發現函數名又恢復正常了。

這樣,顯式調用時只需查找函數名為FuncInDll的函數即可成功。


extern “C”

使用extern “C”關鍵字實際上相當于一個編譯器的開關,它可以將c++語言的函數編譯為c語言的函數名稱。即保持編譯后的函數符號名等于源代碼中的函數名稱。


隱式調用DLL

顯式調用顯得非常復雜,每次都要LoadLibrary,并且每個函數都必須使用GetProcAddress來得到函數指針,這對于大量使用dll函數的客戶是一種困擾。而隱式調用能夠像使用c函數庫一樣使用dll中的函數,非常方便快捷。

下面是一個隱式調用的例子:dll包含兩個文件dll_withlibAndH.cppdll_withlibAndH.h。

代碼如下:dll_withlibAndH.h

extern "C" __declspec(dllexport) void FuncInDll (void);

dll_withlibAndH.cpp

#include <objbase.h>
#include <iostream>
using namespace std;
#include "dll_withLibAndH.h"http://看到沒有,這就是我們增加的頭文件
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;
}

把dll生成一下。


 在進行隱式調用的時候需要在客戶端引入頭文件,并在鏈接時指明dll對應的lib文件(dll只要有函數輸出,則鏈接的時候會產生一個與dll同名的lib文件)位置和名稱。然后如同調用api函數庫中的函數一樣調用dll中的函數,不需要顯式的LoadLibraryGetProcAddress。使用最為方便??蛻舳舜a如下:dll_withlibAndH_client.cpp


#include "dll_withlibAndH.h"
#pragma comment(lib,"dll_withLibAndH.lib") //也可以不用此句,直接在項目->屬性->配置屬性->
                                            //鏈接器->輸入->附加依賴項中加入這個lib的地址
int main(void)
{
    FuncInDll();//只要這樣我們就可以調用dll里的函數了
    return 0;
}

記得把dll_withlibAndH.h,dll_withlibAndH.h.dll,dll_withlibAndH.h.lib放到工程目錄下。其中.h,.lib文件是編譯時需要的,.dll文件是運行時需要的。

__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。源代碼可以在下載的包中找到。你可以編譯鏈接并運行試試。

值得關注的是DLL1DLL2中都使用的一個編碼方法,見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_EXPORTSDLL_DLL2_API,可以確保DLL端的函數用__declspec(dllexport)修飾,而客戶端的函數用__declspec(dllimport)修飾。

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);
    void show(void);
    // TODO: add your methods here.
};

Cpp文件dll_object.cpp如下:

#define DLL_OBJECT_EXPORTS
#include <objbase.h>
#include <iostream>
using namespace std;
#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;
}
void 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;
}


查看生成的dll,可以看到五個符號

動態鏈接庫DLL的編寫

它們分別代表類CDll_Object,類的構造函數,FuncInDll函數,全局變量g_nDll和類的成員函數show。下面是客戶端代碼:dll_object_client.cpp

#include "dll_object.h"
#include <iostream>
using namespace std;
#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;
}

運行這個客戶端可以看到:

動態鏈接庫DLL的編寫

可知,在客戶端成功的訪問了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文件,我也同意這個看法。



終于結尾了,最后還是感謝一下原博主,讓我開開心心的做了一次搬運工!


向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女