[ 前言 ]
本例程演示如何使用 Visual Studio IDE 通过 Microsoft C++ (MSVC) 编写自己的动态链接库 (DLL)。 然后,该演练演示如何从其他 C++ 应用中使用 DLL。 DLL(在基于 UNIX 的操作系统中也称为“共享库”)是最有用的 Windows 组件类型之一 。 可以将其用作共享代码和资源、缩小应用大小的一种方法。 DLL 甚至可使应用更易于维护和扩展。
在本演练中,你将创建一个 DLL 并实现一些数学函数。 然后再创建一个控制台应用来使用 DLL 中的这些函数。 你还将了解 Windows DLL 中使用的一些编程技术和约定。
本演练涵盖以下任务:
在 Visual Studio 中创建 DLL 项目。
将导出的函数和变量添加到该 DLL。
在 Visual Studio 中创建一个控制台应用项目。
在该控制台应用中使用从 DLL 导入的函数和变量。
运行已完成的应用。
与静态链接库一样,DLL 也是按名称导出变量、函数和资源 。 客户端应用导入名称以使用这些变量、函数和资源 。 与静态链接库不同的是,Windows 在加载时或在运行时将应用中的导入连接到 DLL 中的导出,而不是在链接时连接它们。 Windows 需要不属于标准 C++ 编译模型的额外信息才能建立这些连接。 MSVC 编译器实现了一些 Microsoft 专用 C++ 扩展,以提供此额外信息。 接下来我们将介绍这些扩展。
本演练将创建两个 Visual Studio 解决方案;一个生成 DLL,另一个生成客户端应用。 DLL 使用 C 调用约定。 只要平台、调用约定和链接约定匹配,便可从采用其他编程语言编写的应用中进行调用。 客户端应用使用隐式链接 ,其中 Windows 在加载时将应用链接到 DLL。 此链接允许应用调用 DLL 提供的函数,就像调用静态链接库中的函数一样。
本演练并不涵盖一些常见的情况。 此代码不会演示其他编程语言对 C++ DLL 的使用。 它不会演示如何创建纯资源 DLL,也不会演示如何使用显式链接在运行时(而不是在加载时)加载 DLL。 请放心,可以使用 MSVC 和 Visual Studio 来执行所有这些操作。
即使 DLL 的代码是用 C++ 编写的,但我们还是为导出的函数使用了 C 样式接口。 有两个主要原因:首先,许多其他语言支持导入 C 样式函数。 客户端应用不必用 C++ 编写。 其次,它避免了与导出的类和成员函数相关的一些常见陷阱。 导出类时很容易进行难以诊断的错误,因为类声明中引用的所有内容必须具有也导出的实例化。 此限制适用于 DLL,但不适用于静态库。 如果类是纯旧数据样式,则不应遇到此问题。
有关 DLL 的详细信息的链接,请参阅在 Visual Studio 中创建 C/C++ DLL。 有关隐式链接和显式链接的详细信息,请参阅确定要使用的链接方法。 有关创建用于使用 C 语言链接约定的编程语言的 C++ DLL 的信息,请参阅导出 C++ 函数以用于 C 语言可执行文件。 有关如何创建用于 .NET 语言的 DLL 的信息,请参阅从 Visual Basic 应用程序调用 DLL 函数。
先决条件
- 运行 Microsoft Windows 7 或更高版本的计算机。 建议使用最新版本的 Windows 获得最佳开发体验。
Visual Studio 的副本。 有关如何下载和安装 Visual Studio 的信息,请参阅安装 Visual Studio。 运行安装程序时,请务必选中“使用 C++ 的桌面开发” 工作负载。 如果在安装 Visual Studio 时未安装此工作负载,请不要担心。 可以再次运行安装程序并立即安装。
了解使用 Visual Studio IDE 的基础知识。 如果你之前使用过 Windows 桌面应用,可能具备一定的相关知识。 有关简介,请参阅 Visual Studio IDE 功能导览。
了解足够的 C++ 语言基础知识以供继续操作。 别担心,我们不会执行过于复杂的操作。
创建 DLL 项目
在本系列的任务中,将创建一个 DLL 项目,添加代码,并生成它。 首先,启动 Visual Studio IDE,并在需要时登录。 根据使用的 Visual Studio 版本,操作说明会略有不同。 请确保在本页左上角的控件中选择了正确的版本。
1,__cdecl
__cdecl是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定.采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈.因此,实现可变参数的函数只能使用该调用约定.由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大.__cdecl可以写成_cdecl.
2、__stdcall
__stdcall调用约定用于调用Win32 API函数.采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定.由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈.__stdcall可以写成_stdcall.
3、__fastcall
__fastcall约定用于对性能要求非常高的场合.__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈.__fastcall可以写成_fastcall.
最后说明:关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...->C/C++->Code Generation项选择.它们对应的命令行参数分别为/Gd、/Gz和/Gr.缺省状态为/Gd,即__cdecl.当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效.
【第一步】打开VS2019创建新项目。如下图所示。
【第二步】选择“动态链接库(DLL)”点击“下一步”继续。
【第三步】在"项目名称"中输入“MathLibrary”点击“创建”按钮继续。
【第四步】在"项目名称"中输入“MathLibrary”点击“创建”按钮继续。项目创建完成后弹出如下窗口。创建解决方案后,就可以在 Visual Studio 中的“解决方案资源管理器”窗口中看到生成的项目和源文件了 。
【第五步】将头文件添加到 DLL若要为函数创建头文件,请在菜单栏上选择“项目”>“添加新项” 。在“添加新项”对话框的左窗格中,选择“Visual C++” 。 在中间窗格中,选择 “头文件(.h)” 。 指定 MathLibrary.h 作为头文件的名称 。 如下图所示。
【第六步】将如下图所示的代码增加到MathLibrary.h头文件中。以“斐波那契数列”函数为例进行说明。
#ifndef _H_MATHLIBRARY_H
#define _H_MATHLIBRARY_H
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#pragma once
#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif
// The Fibonacci recurrence relation describes a sequence F
// where F(n) is { n = 0, a
// { n = 1, b
// { n > 1, F(n-2) + F(n-1)
// for some initial integral values a and b.
// If the sequence is initialized F(0) = 1, F(1) = 1,
// then this relation produces the well-known Fibonacci
// sequence: 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b.
// This function must be called before any other function.
/*extern "C" MATHLIBRARY_API void fibonacci_init(const unsigned long long a, const unsigned long long b);*/
// Produce the next value in the sequence.
// Returns true on success and updates current value and index;
// false on overflow, leaves current value and index unchanged.
/*extern "C" MATHLIBRARY_API bool fibonacci_next();*/
// Get the current value in the sequence.
/*extern "C" MATHLIBRARY_API unsigned long long fibonacci_current();*/
// Get the position of the current value in the sequence.
/*extern "C" MATHLIBRARY_API unsigned fibonacci_index();*/
MATHLIBRARY_API void _stdcall fibonacci_init(const unsigned long long a, const unsigned long long b);
MATHLIBRARY_API bool _stdcall fibonacci_next();
MATHLIBRARY_API unsigned long long _stdcall fibonacci_current();
MATHLIBRARY_API unsigned _stdcall fibonacci_index();
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // !_H_MATHLIBRARY_H
【第七步】有了如下条件编译代码后,要导出的函数。 extern "C" MATHLIBRARY_API bool _stdcall fibonacci_next();可以改为 MATHLIBRARY_API bool _stdcall fibonacci_next();
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#ifdef __cplusplus
}
#endif // __cplusplus
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#ifdef __cplusplus
}
#endif // __cplusplusextern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。
而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件, extern "C"{ //因为cpp文件默认定义了该宏),则采用C语言方式进行编译。
函数 MATHLIBRARY_API void _stdcall fibonacci_init(const unsigned long long a, const unsigned long long b);中的“_stdcall ”约定也可以去掉,如下图所示编译器中统一设定了编译约定。
【第八步】向 DLL 添加实现
在“解决方案资源管理器”中,右键单击“源文件”节点并选择“添加”>“新建项目” 。 使用上一步中添加新头文件的相同方式,创建名为 MathLibrary.cpp 的新 .cpp 文件 。
在编辑器窗口中,选择 MathLibrary.cpp 的选项卡(如果已打开)。 如果未打开,请在“解决方案资源管理器”中,双击 MathLibrary 项目的“Source Files”文件夹中的 MathLibrary.cpp,将其打开 。
在编辑器中,将 MathLibrary.cpp 文件的内容替换为以下代码:
【 第九步】在CPP文件中包含MathLibrary.h文件。
【第十步】在CPP中增加如下代码。代码中_stdcall 可以注释掉。
// MathLibrary.cpp : Defines the exported functions for the DLL.
#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier
#include <utility>
#include <limits.h>
#include "MathLibrary.h"
// DLL internal state variables:
static unsigned long long previous_; // Previous value, if any
static unsigned long long current_; // Current sequence value
static unsigned index_; // Current seq. position
// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b.
// This function must be called before any other function.
MATHLIBRARY_API void _stdcall fibonacci_init(
const unsigned long long a,
const unsigned long long b)
{
index_ = 0;
current_ = a;
previous_ = b; // see special case when initialized
}
// Produce the next value in the sequence.
// Returns true on success, false on overflow.
MATHLIBRARY_API bool _stdcall fibonacci_next()
{
// check to see if we'd overflow result or position
if ((ULLONG_MAX - previous_ < current_) ||
(UINT_MAX == index_))
{
return false;
}
// Special case when index == 0, just return b value
if (index_ > 0)
{
// otherwise, calculate next sequence value
previous_ += current_;
}
std::swap(current_, previous_);
++index_;
return true;
}
// Get the current value in the sequence.
MATHLIBRARY_API unsigned long long _stdcall fibonacci_current()
{
return current_;
}
// Get the current index position in the sequence.
MATHLIBRARY_API unsigned _stdcall fibonacci_index()
{
return index_;
}
【第十一步】编译代码,没有错误的情况下将会在项目文件夹Release文件下会生成MathLibrary.DLL文件和MathLibrary.lib文件。如下图所示。
第十二步】还是以MFCApplication1项目为例对生成的动态链接库MathLibrary.DLL进行实例应用,将MathLibrary.lib文件放入MFCApplication1项目目录下的“CbLib”文件夹,将"MathLibrary.h"文件放入MFCApplication1项目目录下的“Cbinclude”文件夹将“MathLibrary.DLL”文件放入MFCApplication1项目目录下的“Release”文件夹。在项目MFCApplication1属性"连接器"输入选项,在附加依赖项中增加“MathLibrary.lib”。在所有选项附加依赖库中增加"MathLibrary.lib"。如下图所示。在"MFCApplication1Dlg.h"头文件中中包含"MathLibrary.h"文件。
【第十三步】在MFCApplication1项目对话框上增加一个按钮,将按钮的caption设置为“WIN32_DLL_TEST”。增加一个List Box控件,ID为:IDC_LIST2,为IDC_LIST2增加变量,变量名为:m_ListBox。为按钮WIN32_DLL_TEST增加处理事件。
CString addr_Str;
fibonacci_init(1,1);
do
{
printf("fibonacci_index=%d:fibonacci_current=%I64u", fibonacci_index(), fibonacci_current());
addr_Str.Format(_T("fibonacci_index=%d:fibonacci_current=%I64u"), fibonacci_index(), fibonacci_current());
m_ListBox.AddString(addr_Str);
//m_ListBox.InsertString(fibonacci_index(), addr_Str);
printf("\n");
} while (fibonacci_next());
addr_Str.Format(_T("fibonacci_index=%d Fibonacci sequence values fit in an unsigned 64 - bit integer."), fibonacci_index()+1);
m_ListBox.AddString(addr_Str);
printf("\n");
printf("fibonacci_index = % d Fibonacci sequence values fit in an unsigned 64 - bit integer.", fibonacci_index()+1);
【第十四步】编译项目,如果没有错误,运行项目如下图所示点击“WIN32_DLL_TEST”按钮后List Box将显示相关计算结果。
注意事项:
下面为微软官方的相关例程,本文也是结合官方教程对DLL封装过程中的相关细节进行相关的验证。https://docs.microsoft.com/zh-cn/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-170https://docs.microsoft.com/zh-cn/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-170如下图所示可以看出本例程运行结果和官方教程运行结果完全相同,官方例程是在控制台程序中进行的调用。关于动态链接库的更详细知识会在以后持续更新,DLL是一个非常强大和复杂的系统工程,对他的升入了解并非一朝一夕。