此分步演练演示如何使用 Visual Studio IDE 创建自己的动态链接库 (DLL),这些库是用 Microsoft C++ (MSVC) 编写的。然后,它展示了如何使用另一个C++应用程序中的 DLL。DLL(在基于 UNIX 的操作系统中也称为共享库)是最有用的 Windows 组件类型之一。你可以使用它们来共享代码和资源,并缩小应用的大小。DLL 甚至可以更轻松地服务和扩展应用。
在本演练中,您将创建一个实现某些数学函数的 DLL。然后,你将创建一个使用 DLL 中的函数的控制台应用。您还将了解 Windows 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++ DLL 以用于使用 C 语言链接约定的编程语言的信息,请参阅导出C++函数以在 C 语言可执行文件中使用。有关如何创建用于 .NET 语言的 DLL 的信息,请参见从 Visual Basic 应用程序调用 DLL 函数。
先决条件
- 运行 Microsoft Windows 7 或更高版本的计算机。我们建议使用最新版本的 Windows 以获得最佳开发体验。
- Visual Studio 的副本。有关如何下载和安装 Visual Studio 2015 的信息,请参阅安装 Visual Studio 2015。使用自定义安装来安装C++编译器和工具,因为它们默认情况下未安装。
创建 DLL 项目
在这组任务中,您将为 DLL 创建一个项目,添加代码并生成它。首先,启动 Visual Studio IDE,并在需要时登录。说明略有不同,具体取决于您使用的 Visual Studio 版本。请确保在页左上角的控件中选择了正确的版本。
在 Visual Studio 2015 及更早版本中创建 DLL 项目
-
在菜单栏上,选择"文件>新建>项目"。 -
在"新建项目"对话框的左窗格中,展开"已安装>模板",选择"Visual C++",然后在中心窗格中选择"Win32 控制台应用程序"。在名称编辑框中输入?MathLibrary?以指定项目的名称。保留默认的"位置"和"解决方案名称"值。将"解决方案"设置为"创建新解决方案"。选中"创建解决方案的目录"(如果未选中)。 -
选择"确定"按钮以关闭"新建项目"对话框并启动"Win32 应用程序向导"。 -
选择"下一步"按钮。在"应用程序设置"页上的"应用程序类型"下,选择"DLL"。 -
选择"完成"按钮以创建项目。
向导完成解决方案后,可以在 Visual Studio 的"解决方案资源管理器"窗口中看到生成的项目和源文件。
现在,此 DLL 的功能不多。接下来,您将创建一个头文件来声明 DLL 导出的函数,然后将函数定义添加到 DLL 以使其更有用。
-
若要为函数创建头文件,请在菜单栏上选择"项目>添加新项"。 -
在"添加新项"对话框的左窗格中,选择"可视C++"。在中心窗格中,选择"头文件 (.h)"。指定?MathLibrary.h?作为头文件的名称。 -
选择"添加"按钮以生成空白头文件,该文件将显示在新的编辑器窗口中。 -
将头文件的内容替换为以下代码:
// MathLibrary.h - Contains declarations of math functions
#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();
此头文件声明一些函数以生成广义斐波那契数列,给定两个初始值。调用以生成熟悉的斐波那契数列。fibonacci_init(1, 1)
请注意文件顶部的预处理器语句。DLL 项目的新项目模板将添加到定义的预处理器宏中。在此示例中,Visual Studio 定义了构建 MathLibrary DLL 项目的时间。<PROJECTNAME>_EXPORTS MATHLIBRARY_EXPORTS
定义宏时,宏将在函数声明上设置修饰符。此修饰符告诉编译器和链接器从 DLL 导出函数或变量以供其他应用程序使用。when 未定义(例如,当客户端应用程序包含头文件时),将修饰符应用于声明。此修饰符优化函数或变量在应用程序中的导入。有关详细信息,请参阅?dllexport,dllimport。MATHLIBRARY_EXPORTS MATHLIBRARY_API __declspec(dllexport) MATHLIBRARY_EXPORTS MATHLIBRARY_API __declspec(dllimport)
向 DLL 添加实现
-
在编辑器窗口中,选择?MathLibrary?的选项卡.cpp(如果该选项卡已打开)。如果没有,请在"解决方案资源管理器"中,双击?MathLibrary?项目的"源文件"文件夹中的"MathLibrary.cpp将其打开。 -
在编辑器中,将 MathLibrary.cpp 文件的内容替换为以下代码:
// MathLibrary.cpp : Defines the exported functions for the DLL.
#include "stdafx.h" // use pch.h in Visual Studio 2019 and later
#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.
void 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.
bool 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.
unsigned long long fibonacci_current()
{
return current_;
}
// Get the current index position in the sequence.
unsigned fibonacci_index()
{
return index_;
}
若要验证到目前为止是否一切正常,请编译动态链接库。若要编译,请选择菜单栏上的"生成>生成解决方案"。DLL 和相关编译器输出放置在解决方案文件夹正下方名为?Debug?的文件夹中。如果创建"发布"版本,则输出将放在名为"发布"的文件夹中。输出应如下所示:
1>------ Build started: Project: MathLibrary, Configuration: Debug Win32 ------
1>MathLibrary.cpp
1>dllmain.cpp
1>Generating Code...
1> Creating library C:\Users\username\Source\Repos\MathLibrary\Debug\MathLibrary.lib and object C:\Users\username\Source\Repos\MathLibrary\Debug\MathLibrary.exp
1>MathLibrary.vcxproj -> C:\Users\username\Source\Repos\MathLibrary\Debug\MathLibrary.dll
1>MathLibrary.vcxproj -> C:\Users\username\Source\Repos\MathLibrary\Debug\MathLibrary.pdb (Partial PDB)
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
恭喜,您已经使用 Visual Studio 创建了一个 DLL!接下来,你将创建一个使用 DLL 导出的函数的客户端应用。
创建使用 DLL 的客户端应用
创建 DLL 时,请考虑客户端应用可能如何使用它。若要调用函数或访问由 DLL 导出的数据,客户端源代码必须在编译时具有可用的声明。在链接时,链接器需要信息来解析函数调用或数据访问。DLL 在导入库中提供此信息,导入库是一个包含有关如何查找函数和数据的信息的文件,而不是实际的代码。在运行时,DLL 必须可供客户端使用,位于操作系统可以找到的位置。
无论是你自己的还是来自第三方的,你的客户端应用项目都需要几条信息才能使用 DLL。它需要查找声明 DLL 导出的标头、链接器的导入库以及 DLL 本身。一种解决方案是将所有这些文件复制到客户端项目中。对于在客户端开发过程中不太可能更改的第三方 DLL,此方法可能是使用它们的最佳方式。但是,当您同时生成 DLL 时,最好避免重复。如果创建正在开发的 DLL 文件的本地副本,则可能会意外更改一个副本中的头文件,但不会更改另一个副本中的头文件,或者使用过期的库。
若要避免代码不同步,建议将客户端项目中的包含路径设置为直接包含 DLL 项目中的 DLL 头文件。此外,在客户端项目中设置库路径以包含 DLL 项目中的 DLL 导入库。最后,将生成的 DLL 从 DLL 项目复制到客户端生成输出目录中。此步骤允许客户端应用使用生成的相同 DLL 代码。
在 Visual Studio 2015 中创建客户端应用
-
若要创建使用你创建的 DLL 的C++应用,请在菜单栏上选择"文件">"新建>项目"。 -
在"新建项目"对话框的左窗格中,选择"已安装>模板"下的"Win32?>?Visual C++"。在中心窗格中,选择"Win32 控制台应用程序"。在"名称"编辑框中指定项目的名称?MathClient。保留默认的"位置"和"解决方案名称"值。将"解决方案"设置为"创建新解决方案"。选中"创建解决方案的目录"(如果未选中)。 -
选择"确定"按钮以关闭"新建项目"对话框并启动"Win32 应用程序向导"。在"Win32 应用程序向导"对话框的"概述"页上,选择"下一步"按钮。 -
在"应用程序设置"页上的"应用程序类型"下,选择"控制台应用程序"(如果尚未选择)。 -
选择"完成"按钮以创建项目。
向导完成后,将为您创建一个最小的控制台应用程序项目。主源文件的名称与您之前输入的项目名称相同。在此示例中,它被命名为?MathClient.cpp。您可以生成它,但它尚未使用您的 DLL。
接下来,若要在源代码中调用 MathLibrary 函数,您的项目必须包含?MathLibrary.h?文件。可以将此头文件复制到客户端应用项目中,然后将其作为现有项添加到项目中。此方法可能是第三方库的不错选择。但是,如果同时处理 DLL 和客户端的代码,则头文件可能会不同步。若要避免此问题,请将项目中的"其他包含目录"路径设置为包含原始标头的路径。
-
右键单击"解决方案资源管理器"中的"MathClient"节点以打开"属性页"对话框。 -
在"配置"下拉框中,选择"所有配置"(如果尚未选中)。 -
在左窗格中,选择"配置属性">"C/C++>常规"。 -
在属性窗格中,选择"其他包含目录"编辑框旁边的下拉控件,然后选择"编辑"。 -
双击"其他包含目录"对话框的顶部窗格以启用编辑控件。或者,选择文件夹图标以创建新条目。 -
在编辑控件中,指定?MathLibrary.h?头文件位置的路径。可以选择省略号 (...) 控件以浏览到正确的文件夹。 还可以输入从客户端源文件到包含 DLL 头文件的文件夹的相对路径。如果按照说明将客户端项目放在与 DLL 不同的解决方案中,则相对路径应如下所示: ..\..\MathLibrary\MathLibrary 如果 DLL 和客户端项目位于同一解决方案中,则相对路径可能如下所示: ..\MathLibrary 当 DLL 和客户端项目位于其他文件夹中时,请调整相对路径以匹配。或者,使用省略号控件浏览该文件夹。 -
在"其他包含目录"对话框中输入头文件的路径后,选择"确定"按钮。在"属性页"对话框中,选择"确定"按钮以保存更改。
现在,您可以包含?MathLibrary.h?文件,并在客户端应用程序中使用它声明的函数。使用以下代码替换?MathClient.cpp?的内容:
// MathClient.cpp : Client app for MathLibrary DLL.
// #include "pch.h" Uncomment for Visual Studio 2017 and earlier
#include <iostream>
#include "MathLibrary.h"
int main()
{
// Initialize a Fibonacci relation sequence.
fibonacci_init(1, 1);
// Write out the sequence values until overflow.
do {
std::cout << fibonacci_index() << ": "
<< fibonacci_current() << std::endl;
} while (fibonacci_next());
// Report count of values written before overflow.
std::cout << fibonacci_index() + 1 <<
" Fibonacci sequence values fit in an " <<
"unsigned 64-bit integer." << std::endl;
}
此代码可以编译,但不能链接。如果现在生成客户端应用,则错误列表会显示多个 LNK2019 错误。这是因为您的项目缺少一些信息:您尚未指定您的项目依赖于?MathLibrary.lib?库。而且,您还没有告诉链接器如何查找?MathLibrary.lib?文件。
若要解决此问题,可以将库文件直接复制到客户端应用项目中。链接器将自动查找并使用它。但是,如果库和客户端应用都在开发中,则可能会导致一个副本中的更改在另一个副本中未显示。若要避免此问题,可以设置"其他依赖项"属性,以告知生成系统你的项目依赖于?MathLibrary.lib。而且,您可以在项目中设置"其他库目录"路径,以便在链接时包含原始库的路径。
将 DLL 导入库添加到项目中
-
右键单击"解决方案资源管理器"中的"MathClient"节点,然后选择"属性"以打开"属性页"对话框。 -
在"配置"下拉框中,选择"所有配置"(如果尚未选中)。它确保任何属性更改都适用于"调试"和"发布"版本。 -
在左窗格中,选择"配置属性">"链接器>输入"。在属性窗格中,选择"其他依赖项"编辑框旁边的下拉控件,然后选择"编辑"。 -
在"其他依赖项"对话框中,将?MathLibrary.lib?添加到顶部编辑控件的列表中。 -
选择"确定"返回到"属性页"对话框。 -
在左窗格中,选择"配置属性">"链接器>常规"。在属性窗格中,选择"其他库目录"编辑框旁边的下拉控件,然后选择"编辑"。 -
在"其他库目录"对话框的顶部窗格中双击以启用编辑控件。在编辑控件中,指定?MathLibrary.lib?文件位置的路径。默认情况下,它位于 DLL 解决方案文件夹正下方名为"调试"的文件夹中。如果创建发布版本,则该文件将放在名为"发布"的文件夹中。可以使用该宏,以便链接器可以找到您的 DLL,无论您创建哪种类型的生成。如果按照说明将客户端项目放在与 DLL 项目不同的解决方案中,则相对路径应如下所示:$(IntDir) ..\..\MathLibrary\$(IntDir) 如果 DLL 和客户端项目位于其他位置,请调整要匹配的相对路径。 -
在"其他库目录"对话框中输入库文件的路径后,选择"确定"按钮返回到"属性页"对话框。选择"确定"以保存属性更改。
客户端应用现在可以成功编译和链接,但它仍然没有运行所需的一切。当操作系统加载你的应用时,它会查找 MathLibrary DLL。如果在某些系统目录、环境路径或本地应用程序目录中找不到 DLL,则加载将失败。根据操作系统的不同,您将看到如下错误消息:
避免此问题的一种方法是将 DLL 复制到包含客户端可执行文件的目录中,作为生成过程的一部分。可以向项目中添加生成后事件,以添加将 DLL 复制到生成输出目录的命令。此处指定的命令仅在 DLL 丢失或已更改时才复制该 DLL。它使用宏根据生成配置在"调试"或"发布"位置之间复制。
在生成后事件中复制 DLL
-
右键单击"解决方案资源管理器"中的"MathClient"节点,然后选择"属性"以打开"属性页"对话框。 -
在"配置"下拉框中,选择"所有配置"(如果尚未选中)。 -
在左窗格中,选择"配置属性">生成事件>生成后事件"。 -
在属性窗格中,选择"命令行"字段中的编辑控件。如果按照说明将客户端项目放在与 DLL 项目不同的解决方案中,请输入以下命令: xcopy /y /d "..\..\MathLibrary\$(IntDir)MathLibrary.dll" "$(OutDir)" 如果 DLL 和客户端项目位于其他目录中,请更改 DLL 的相对路径以匹配。 -
选择"确定"按钮以保存对项目属性所做的更改。
现在,客户端应用具有生成和运行所需的一切。通过在菜单栏上选择"生成>生成解决方案"来生成应用程序。Visual Studio 中的"输出"窗口应具有类似于以下示例的内容,具体取决于您的 Visual Studio 版本:
1>------ Build started: Project: MathClient, Configuration: Debug Win32 ------
1>MathClient.cpp
1>MathClient.vcxproj -> C:\Users\username\Source\Repos\MathClient\Debug\MathClient.exe
1>1 File(s) copied
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
恭喜,你已创建一个在 DLL 中调用函数的应用程序。现在,运行应用程序以查看其功能。在菜单栏上,选择"调试">"启动而不调试"。Visual Studio 打开一个命令窗口,供程序在其中运行。输出的最后一部分应如下所示:
按任意键关闭命令窗口。
现在,您已经创建了 DLL 和客户端应用程序,可以进行试验了。尝试在客户端应用的代码中设置断点,并在调试器中运行应用。查看单步执行库调用时会发生什么情况。将其他函数添加到库中,或编写使用 DLL 的其他客户端应用。
部署应用时,还必须部署它使用的 DLL。要使你生成的 DLL 或你从第三方包含的 DLL 可用,最简单的方法是将它们放在与应用相同的目录中。这称为应用本地部署。有关部署的详细信息,请参阅?Visual C++中的部署。
|