且看一文梳理VS2019中dll的建立使用

唯有自己強大發表於2021-08-16

動態連結庫(dll)

Windows下有靜態連結(lib)庫和動態連結庫(dll)兩種共享程式碼的方式。

本文將介紹dll的應用場景,以及在vs2019平臺下的生成和使用。

今天的筆記內容說的是平時經常能看見的,執行 VS 專案的時候老在下方載入的 .dll 。包括一小部分的理論和超大部分的實操。


 [What] dll是什麼

動態連結庫(Dynamic Link Library)又稱為“應用程式擴充套件”,在windows系統中,大多數應用程式並非僅有一個可執行檔案exe,同時也包含一些相對獨立(模組化)的dll檔案。dll中存放函式程式碼實現,exe中存放dll中相應函式程式碼的地址,而且dll中的程式碼可以被多個exe呼叫而在記憶體中僅保留一份拷貝,從而節省了記憶體空間。

[How] 如何生成dll

步驟<1>:建立新專案

 步驟<2>:配置新專案

 輸入“專案名稱”,然後選擇工程“位置”,“解決方案名稱”與“專案名稱”相同,是自動生成的,如果沒有特殊需求建議不要修改,不要勾選“將解決方案和專案放在同一目錄中”,最後點選“建立”按鈕。

步驟<3>:匯出DLL

 vs官方文件中提供了兩種方式可以匯出dll中的函式:

  • 關鍵字__declspec(dllexport):操作簡單,但通用性較差。可見,vs建立dll專案時預設使用了該方式
  • 模組定義檔案(.def):通用性(指給其他語言eg. Java、C#呼叫)好,但操作相對複雜

 使用關鍵字__declspec(dllexport)


 (1)首先新建立標頭檔案“CreateDll.h”,它的作用是用來宣告需要匯出的函式介面。

#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif 

//匯出類
class MYDLL_API Rectangle
{
public:
    double getarea(double w, double h);
    void   print();

};

//匯出函式
extern"C" MYDLL_API int __stdcall mysum(int a, int b);

(2)然後我們需要在‘CreateDll.cpp’中實現在‘CreateDll.h’中被宣告的函式,程式碼如下:

#include "pch.h"
#include "CreateDll.h"
#include<iostream>
double Rectangle::getarea(double w, double h)
{
    return w * h;
}
void Rectangle::print()
{
    std::cout << "已被列印";
}
int __stdcall mysum(int a, int b)
{
    return a + b;
}

(3)點選重新生成解決方案,即在debug目錄下生成MyDll.lib和MyDll.dll

程式碼分析:

  • __declspec(dllexport)此修飾符告訴編譯器和連結器被它修飾的函式或變數需要從DLL匯出,以供其他應用程式使用;

與其相對的還有一句程式碼是__declspec(dllimport),此修飾符的作用是告訴編譯器和連結器被它修飾的函式或變數需要從DLL匯入

  • extern "C"的作用是告訴編譯器將被它修飾的程式碼按C語言的方式進行編譯 

這是由於C語言沒有過載,不會改變函式名。而C++中有過載,在編譯過程中會根據返回值和引數修改函式名。

  • __stdcall定義匯出函式入口點呼叫約定為_stdcall

C編譯器的函式名修飾規則:

  1. __stdcall呼叫約定,編譯器和連結器會在輸出函式名前加上一個下劃線字首,函式名後面加上一個“@”符號和其引數的位元組數,例如 _functionname@number。
  2. __cdecl呼叫約定僅在輸出函式名前加上一個下劃線字首,例如_functionname。
  3. __fastcall呼叫約定在輸出函式名前加上一個“@”符號,後面也是一個“@”符號和其引數的位元組數,例如@functionname@number

模組定義檔案(.def)


(1)新建.def檔案

 VS會自動新增.def檔案為連結器輸入:

 (2)實現一個dll函式

(3)編寫.def檔案如下

 

[How] 如何呼叫dll

新建一個控制檯應用,在其中呼叫上述生成的dll。

呼叫dll有兩種連結方式:隱式連結顯式連結無論哪種方式都要求將dll和exe放在同一目錄下


 隱式連結


  •  隱式連結需要三個檔案:.h檔案、.lib檔案 和 .dll檔案。
  • 對於.h檔案: 屬性頁->C/C++->附加包含目錄 新增路徑並引用。(或者直接引用絕對路徑)
  • 對於.lib檔案(有兩種新增方法)
  1. 屬性頁->連結器->常規->附加庫目錄( 新增.lib檔案路徑);  屬性頁->連結器->輸入->附加依賴項 (新增.lib檔名)
  2. 直接用#pragma comment(lib,"MyDll.lib) (需要將該lib檔案放到與exe同目錄下)

在配置好檔案後編寫程式碼,呼叫dll:

#include"CreateDll.h"
#include<iostream>
#pragma comment(lib,"MyDll.lib")
int main()
{
    Rectangle rect;
    std::cout << "矩形面積:" << rect.getarea(3, 2)<<std::endl;
    rect.print();
    std::cout << "二數相加" << mysum(3, 2);
    return 0;
}


顯式連結


  •  顯式連結只需要一個檔案:.dll檔案。
  • 所謂顯式連結,就是直接呼叫WIN32 API函式LoadLibraryGetProcAddressFreeLibrary顯式地裝載、解除安裝dll。

顯式連結整體思路:

  1. 宣告標頭檔案<windows.h>,說明我想用windows32方法來載入和解除安裝DLL
  2. 然後用typedef定義一個指標函式型別(這個指標型別,要和你呼叫的函式型別和引數保持一致)
  3. 定義一個控制程式碼例項,用來取DLL的例項地址。(HMODULE hdll;)
  4. 載入目標DLL,即 LoadLibrary()函式,將DLL載入到控制程式碼例項,若成功則返回該DLL模組的控制程式碼,否則返回NULL
  5. 獲得匯出函式的地址,即GetProcAddress()函式,成功時返回函式地址,否則返回NULL
  6. 呼叫匯出函式
  7. 解除安裝dll
#include<iostream>
#include<Windows.h>
typedef int(*Pmysum)(int a, int b);//定義一個指標函式型別
int main()
{
HMODULE Hdll = LoadLibrary(L"MyDll.dll");//獲取dll地址
if (Hdll!=NULL)
{
    Pmysum mysunm = (Pmysum)GetProcAddress(Hdll, "mysum");//獲取dll中的函式地址
    if (mysunm !=NULL)
    {
        std::cout << "呼叫兩變數相加函式:"<<mysunm(3, 2);
    }
}
FreeLibrary(Hdll);//解除安裝dll
return 0;
}

 這也暴露出顯式連結的一個弊端:要求開發人員必須清楚地知道呼叫函式的匯出名稱和傳參格式。extern "C"和def檔案相當於給函式重新命名,如果想呼叫預設c++方式匯出的函式,就要用那一長串修飾後的函式名。


例項:用顯式呼叫dll中的類

首先需要強調,當使用某個類時一般目的有二:例項化成物件或者繼承它產生新類。對於前者,我們可以構造一個抽象類來連線呼叫方和DLL。

一,建立dll庫

1️⃣建立動態連結庫專案,新建一個介面類Interface

 2️⃣在Interface.h

#ifdef INTERFACE_EXPORTS
#define INTERFACE_API __declspec(dllexport)
#else
#define INTERFACE_API __declspec(dllimport)
#endif

#pragma once

class Interface
{
public:
    virtual void ShowMsg() = 0; // 將呼叫方需要呼叫的成員函式宣告成純虛擬函式
    virtual ~Interface() {};// 抽象類的虛解構函式
};
extern "C" INTERFACE_API Interface * Export(void); //外部介面

3️⃣Interface.cpp( 通過匯出外部介面向呼叫方提供指向派生類Test物件的基類指標)

#include "pch.h"
#include "Interface.h"
#include"Test.h"
// 通過匯出函式形式向呼叫方提供指向派生類物件的基類指標
Interface* Export(void)
{
    return (Interface*)new Test();
}

4️⃣將真正要呼叫的類Test宣告成抽象類 Interface 的派生類

Test.h

#pragma once
#include "Interface.h"
#include <string>
class Test:public Interface
{
public:
    Test();
    virtual ~Test();
    virtual void ShowMsg(void);//重寫虛擬函式
private:
    std::string s;
};

Test.cpp

#include "pch.h"
#include "Test.h"
#include<iostream>
Test::Test()
{
    s = "hello form dll";
}

Test::~Test()
{
    std::cout << "destroy";
}

void Test::ShowMsg()
{
    std::cout << s << std::endl;
}

二,顯式呼叫dll

建立一個空專案testdll,將生成的Mydll.dll和Interface.h放入testdll的目錄下

 在testdll專案中新建rundll.cpp。動態呼叫dll

#include <Windows.h>
#include"Interface.h" // 包含抽象類從而使用介面
#include<iostream>

using pExport = Interface * (*)(void); // 定義指向匯出函式的指標型別
int main()
{
    HINSTANCE hDll = LoadLibrary(L"Mydll.dll");// 載入DLL庫檔案,DLL名稱和路徑用自己的
    if (hDll !=NULL)
    {
        pExport Get = (pExport)GetProcAddress(hDll, "Export");// 將指標指向函式首地址
        if (Get == NULL)
        {
            std::cout << "load address fail \n";
            return -1;
        }
        Interface* t = Get();// 呼叫匯出函式獲得抽象類指標
        t->ShowMsg();// 通過該指標呼叫類成員函式
        delete t; // 釋放DLL中生成的物件
        FreeLibrary(hDll); //釋放庫控制程式碼
    }
    system("pause");
    return 0;
}

此時需要注意兩點:

  • 我們需要把Interface.h放在UseDLL工程目錄下
  • 如果編譯時出現:無法將引數 1 從“const char [14]”轉換為“LPCWSTR”的錯誤,則我們需要點選專案屬性,常規-》字符集-》改為“未設定”即可

實際上整個專案的方法是Interface完成了介面的設定,而具體的實現在test中進行,真正使用了類的抽象性和多型性,封閉性。

相關文章