且看一文梳理VS中dll的创建使用

时间:2024-02-15 17:12:19

动态链接库(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中进行,真正使用了类的抽象性和多态性,封闭性。