ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VS2019 DLL动态库连接实例:从导出到加载的完整实践

VS2019 DLL动态库连接实例:从导出到加载的完整实践 简介一份面向Visual Studio 2019开发者的DLL动态库连接实例图文教程系统梳理从动态库项目创建到控制台程序调用函数并验证结果的完整链路适合初学动态链接库或需要快速熟悉VS2019工程配置的读者也能帮助已有一定经验的开发者快速定位常见连接问题。资源包内仅含一个PDF文档共446KB步骤配图清晰内容紧凑便于边看边操作。教程以名为mydll的动态库项目为主线先在预编译头文件pch.h中声明加法函数Add和减法函数Sub再在pch.cpp中实现函数体编译后生成lib与dll文件之后转到新建的控制台应用说明如何配置工程属性中的附加包含目录、附加库目录和附加依赖项准备好所需头文件与静态库路径并将生成的dll复制到可执行文件所在目录最终借助include预处理命令和pragma comment指令关联头文件与lib直接调用DLL中的导出函数形成动态库调用闭环。目前已有5000余人学习浏览对希望掌握VS2019下DLL生成、连接与调用全流程的开发者具有直接参考价值。1. 从一张报错截图说起Visual Studio 2019 DLL动态库连接实例在解决什么工程师手里的 DLL 通常自带两种状态要么是别人拷过来的一个绿色小文件配一份几十行的接口说明双击程序却弹 0xC0000135要么是自己刚生成完的产物换个解决方案就无论如何连不上链接器报一堆 LNK2019。这个标题Visual Studio 2019 DLL动态库连接实例讲的就是这两类人共同卡住的环节DLL 已经导出了调用方在 VS 2019 里到底怎么配置工程才能在链接时找到符号、运行时加载成功。我不会把截图铺满全文而是把每步操作落到菜单路径、配置项和可直接编译的代码上再解释为什么这么配。新手可以照着做一遍把工程跑通老手可以拿它当排查对照清单少走弯路。2. DLL 导出与名字修饰连接的第一步在生成 DLL 那边很多人以为“连接”是调用方工程的事其实 80% 的“无法解析的外部符号”在生成 DLL 那侧就已经注定了。调用方通过头文件声明、导入库 lib、以及 DLL 导出表里的符号名三者名字对不上VS 2019 连接器直接抛 LNK2019 或 LNK2001。这一章先把生成侧的三个关键点讲清楚它们是后续连接成功的前提。2.1__declspec(dllexport)与 .def 文件两种导出方式怎么选最常见的做法是直接在头文件里做条件宏一段代码同时兼顾 DLL 编译方和调用方两种视角。// MathLibrary.h #pragma once #ifdef MATHLIBRARY_EXPORTS #define MATHLIBRARY_API __declspec(dllexport) #else #define MATHLIBRARY_API __declspec(dllimport) #endif extern C MATHLIBRARY_API int add(int a, int b); extern C MATHLIBRARY_API int subtract(int a, int b);MATHLIBRARY_EXPORTS是 VS 2019 创建 DLL 工程时自动加的预处理器定义只有生成 DLL 那个项目里存在。编译 DLL 时头文件走dllexport分支函数被写进 DLL 的导出表调用方包含同一份头文件时宏未定义走dllimport分支链接器就知道这些符号来自外部。这里要注意不要图省事把两个分支删掉只留dllexport那会让调用方工程按错误的语义去处理声明问题更隐蔽。另一种不常被新手注意的路径是用 .def 文件导出适合第三方库或需要精确控制导出序号的场景。LIBRARY MathLibrary EXPORTS add subtract.def 方式不需要在声明上加__declspec(dllexport)但文件名和导出项要单独维护而且 C 修饰名问题依然存在该写extern C还是得写。方案适用场景维护成本导出名控制__declspec(dllexport)自己维护源码日常开发首选低宏自动切换受调用约定和extern C影响.def 文件旧库改造、按序号导出中名字单独维护可以在 .def 中固定名字和序号2.2 extern C 与 __stdcallx86 下函数名会变成什么C 编译函数时默认做名字修饰int add(int, int)在 x86 下的修饰名形如?addYAHHZ。如果没有extern CDLL 导出表里存的就是这串名字调用方头文件里写的却是普通add两边对不上连接必然失败。这就是为什么所有跨模块导出我都会在声明外加extern C把名字修饰拉回 C 风格。而__stdcall在 x86 上会额外把参数总字节数追加到导出名后面。比如两个 int 参数是 8 字节导出名会变成_add8。这时候调用方如果用默认的__cdecl去声明同一个函数链接器会找_add同样报错。声明写法x86 下导出名extern C int add(int,int)默认__cdecl_addextern C int __stdcall add(int,int)_add8不加extern C的普通 C 函数形如?addYAHHZx64 上只有一种调用约定__stdcall会被忽略名字也不再带下划线和8后缀。所以同一份代码在 x64 下往往能过切到 x86 就翻车。遇到 Debug 配置默认跑 x86、链接报 LNK2019 时先把平台切到 x64 验证再回头对着这张表查声明比乱调项目属性高效得多。2.3 dumpbin /exports 查看导出表确认符号可被外部连接VS 2019 自带 dumpbin不需要额外安装。打开“x64 Native Tools Command Prompt for VS 2019”或者直接从开始菜单启动“Developer Command Prompt”执行dumpbin /exports MathLibrary.dll输出里会有一个name列列出 DLL 实际导出的全部符号。你在这里看到的函数名才是调用方最终能连上的名字。如果看到的是?add...这种修饰名回头给声明补extern C如果看到_add8说明调用约定确实是__stdcall调用方声明也得跟着改。提示dumpbin 支持全路径直接写dumpbin /exports D:\build\MathLibrary.dll也一样。从这一步开始你就拥有了一套不依赖 IDE 界面的验证方法。3. 在 VS 2019 中创建 DLL 工程并拿到 h/lib/dll 三件套理论立住之后现在从建工程开始走一遍。很多人卡在源头上项目模板选错了或者预编译头设置不对导致编译都过不去。这一章把 VS 2019 的操作路径和生成产物的来龙去脉说清楚。3.1 用“动态链接库(DLL)”模板建工程模板自带哪些文件菜单路径文件 - 新建 - 项目 - 语言选 C - 搜索“动态链接库”选择“动态链接库(DLL)”模板工程名我习惯叫 MathLibrary。这里要和“Windows 桌面应用程序”区分开那个模板生成的是 exe后面所有配置都不成立。VS 2019 会默认生成四个文件dllmain.cpp、pch.h、pch.cpp、framework.h。dllmain.cpp里的DllMain是 DLL 的入口点处理进程和线程的附着、分离事件平时不用写任何业务代码pch.h和pch.cpp是预编译头的一部分新加的源文件里必须include pch.h否则编译会报 C1010。你可以把预编译头整个关掉但工程默认开启时不要跟编译器对着干。3.2 写导出头文件与实现注意自动生成的项目宏在解决方案资源管理器里右键项目 - 添加 - 新建项 - 头文件把 2.1 那段MathLibrary.h加进去。再添加一个MathLibrary.cpp写实现// MathLibrary.cpp #include pch.h #include MathLibrary.h int add(int a, int b) { return a b; } int subtract(int a, int b) { return a - b; }#include pch.h必须放在第一行这是预编译头的硬性要求。MathLibrary.h里的函数现在已经被MATHLIBRARY_EXPORTS宏标记成dllexport编译后就会写进 DLL 导出表。如果你改过工程名宏名也会跟着变比如工程叫 Foo宏就是FOO_EXPORTS自己手写头文件时别把宏名记错。3.3 生成后拿到 .dll、.lib、.h 三个文件别混淆导入库与静态库按 CtrlShiftB 生成解决方案默认输出到解决方案目录下的x64\Debug或x64\Release取决于你配的解决方案平台。里面出现三个关键文件MathLibrary.dll、MathLibrary.lib、MathLibrary.pdb再加上工程里的MathLibrary.h就是完整的“三件套”。MathLibrary.lib是导入库不是静态库。它里面没有 add 的代码实现只有一段桩信息告诉连接器“add 这个符号在 MathLibrary.dll 里偏移是多少”。连接器生成 exe 时读 .lib程序运行时 Windows 加载器读 .dll两个文件缺哪个都不行。很多新手把 .lib 当成静态库删了或者只拷贝 .dll 去别的机器运行时报错后一脸茫然原因就在这里。文件谁在哪个阶段用缺失时现象MathLibrary.h编译器编译调用方源码时C2065 未声明的标识符MathLibrary.lib连接器生成 exe 时LNK2019 或找不到 libMathLibrary.dll运行时加载0xC0000135 或系统弹窗3.4 顺手改掉源码编码把 VS 2019 默认代码页调成 UTF-8如果你在 DLL 源码里写了中文注释或者中文字符串VS 2019 在简体中文系统上默认按本地代码页读取源文件经常报警告 C4819或者编出来的字符串在别的机器上显示成乱码。运行时字符集设置项目属性 - 常规 - 字符集管的是宽窄字符转换跟源码文件本身的编码是两码事。真正要改的是文件编码菜单栏 文件 - 另存为 - 保存按钮旁边的箭头 - 编码保存 - 选“UTF-8 带签名”。如果“编码保存”选项没有直接出现去 工具 - 自定义 - 命令 - 文件 里把“高级保存选项”命令拖到菜单上。UTF-8 带 BOM 是 Windows 生态下最稳的源码编码VS 2019 能正确识别MSVC 编译器也不会再纠结代码页。4. 调用方连接 DLL 的两种写法隐式连接与 LoadLibrary 显式加载这是标题里“连接”二字的核心。DLL 造出来了调用方怎么连上去业界通行的做法分两种隐式连接编译期通过 lib 绑定和显式连接运行期 LoadLibrary。两种都值得掌握因为它们应对的场景不同。4.1 调用方新建空项目先把位数和配置与 DLL 对齐菜单路径文件 - 新建 - 项目 - C - 空项目或者“控制台应用”然后添加一个main.cpp。建好后第一件事不是写代码而是看工具栏上的解决方案平台是不是 x64和 DLL 生成时的平台保持一致。Windows 一个进程只能有一种位数exe 是 x64 就加载不了 x86 的 DLL运行时直接报“模块映像格式不正确”。Debug/Release 也尽量一致混用虽然偶尔能运行但调试时会踩到代码优化不一致的坑。4.2 隐式连接附加包含目录、附加库目录、附加依赖项三步配置把MathLibrary.h放到调用方工程的 include 目录然后在main.cpp里写调用代码#include iostream #include MathLibrary.h int main() { int r add(10, 5); std::cout r std::endl; return 0; }编译能过但连接会报找不到add符号因为还没告诉链接器去哪找。VS 2019 需要配置三处项目属性 - C/C - 常规 - 附加包含目录填MathLibrary.h所在目录让编译器找得到声明。项目属性 - 链接器 - 常规 - 附加库目录填MathLibrary.lib所在目录让连接器找得到导入库。项目属性 - 链接器 - 输入 - 附加依赖项填MathLibrary.lib显式声明需要链接这个库。不想每次开属性页折腾的话第三条可以换成代码里的指令#pragma comment(lib, MathLibrary.lib)#pragma comment(lib, ...)是写给连接器看的等效于在附加依赖项里填库名。但注意第二条的“附加库目录”仍然要配否则连接器根本找不到这个 .lib 文件。生成 exe 后再把MathLibrary.dll复制到 exe 同级目录。VS 2019 调试时默认工作目录是项目目录.vcxproj所在目录它也会在那找 DLL。很多人代码明明没问题、一运行就报找不到 DLL多半是把 dll 放在了别处。4.3 显式连接LoadLibrary GetProcAddress 最小调用代码当你想在程序启动时不强制依赖 DLL、运行时按需加载或者做插件系统时用显式连接。它不需要 .lib不需要附加依赖项配置全部在代码里完成#include windows.h #include iostream typedef int (*AddFunc)(int, int); int main() { HMODULE hMod LoadLibraryW(LMathLibrary.dll); if (!hMod) { std::cerr LoadLibrary failed, code GetLastError() std::endl; return -1; } AddFunc add reinterpret_castAddFunc(GetProcAddress(hMod, add)); if (!add) { FreeLibrary(hMod); std::cerr GetProcAddress failed, code GetLastError() std::endl; return -2; } std::cout result: add(3, 4) std::endl; FreeLibrary(hMod); return 0; }LoadLibraryW用宽字符版本避免路径里带中文时出现 ANSI 编码问题。GetProcAddress返回的是FARPROC必须强转成与目标函数签名一致的函数指针这里就是int (*)(int, int)。签名不一致时参数会被按错误方式解读轻则返回垃圾值重则栈被写坏直接崩溃。显式连接的典型场景就是加载onnxruntime.dll、OpenGL 动态库这类第三方运行时你不用在连接期绑定它们但要在调用前检查每个句柄和函数指针是否有效。GetLastError()的返回值有很强的指引性错误码含义优先排查方向126找不到指定模块DLL 路径、位数、依赖链127找不到指定过程导出函数名是否被修饰过回到 2.2 查1114DLL 初始化例程失败DLL 的DllMain或它依赖的静态库初始化失败Python 开发者常见的OSError: [WinError 1114] 动态链接库(DLL)初始化例程失败报错信息里说某个 dll “or one of its dependencies”本质就是这条链路里某一个依赖初始化没通过。C 调用 DLL 时出 1114排查思路完全一样去看 DLL 的依赖项而不是怀疑调用代码。4.4 运行时报错 126 / 127 / 1114 时的排查次序按顺序排查多数问题三步内能定位位数确认任务管理器看 exe 进程是 64 位还是 32 位再用后面 5.3 的方法确认 DLL 位数两者必须一致。依赖链检查在 DLL 所在目录执行dumpbin /dependents MathLibrary.dll看它还依赖哪些 DLL。缺 VC 运行库时目标机器装对应版本的 Visual C Redistributable 就能解决。加载路径Windows 按“exe 目录 - 系统目录 - PATH”的顺序找 DLL。只把 dll 放进工程目录然后分发 exe到没有装 VS 的机器上必然报缺库。最后提一个高发问题dll 冲突。系统目录或 PATH 里如果已经存在另一个同名旧版 dll即使你的 exe 目录里有正确版本某些加载顺序下也可能拿到旧文件。国产软件安装目录里这种同名覆盖尤其常见装完某个软件后原本正常的程序莫名弹 0xC0000135多半是系统目录被写入了同名旧版。遇到这种情况先用 Process Explorer 看已加载模块的真实路径别急着下“DLL 文件损坏”的结论。5. 用 dumpbin 和模块窗口验证 DLL 连接成功的三个技巧连接成功不是编译通过就算数要验证到“运行时真的加载了正确路径的 DLL”这一层。以下三个手段按验证深度递进也是我每次换机器、换编译器版本后必做的体检项。5.1 模块窗口调试时确认 DLL 确实加载进来了在main.cpp里add(10, 5)那行设个断点按 F5 调试运行断点命中后打开菜单 调试 - 窗口 - 模块。列表里找到MathLibrary.dll看它的“路径”列。如果路径不是你以为的那个目录说明加载器取的是 PATH 里另一个同名文件问题立刻暴露。这个窗口同时显示 DLL 的版本号和符号状态右键 - 加载符号可以顺手把 PDB 配上之后 F11 就能直接步进 DLL 内部源码。5.2 dumpbin /imports 反查 exe验证编译期的连接链路隐式连接成功后用 dumpbin 从另一个方向验证dumpbin /imports MyApp.exe输出里会有一段MathLibrary.dll的说明下面列出add、subtract两个符号。这表明 exe 的导入表里已经写死了对 MathLibrary 的依赖。它和 2.3 的dumpbin /exports是同一枚硬币的两面exports 管 DLL 产出什么imports 管 exe 消费什么。连接期缺 .lib 会在生成时报 LNK运行期缺 dll 会弹 0xC0000135这两条命令正好各管一个阶段能帮你立刻区分是“没连上”还是“没加载到”。5.3 dumpbin /headers 判断 DLL 位数终结 32/64 位不匹配dumpbin /headers MathLibrary.dll | findstr /i machine输出里会出现machine (8664)或machine (14C)前者是 x64后者是 x86。我用这个作为终极验证手段只要 DLL 产物位数正确、调用方导入表正确、运行时模块窗口路径正确、位数一致连接实例就已经闭环。之后再遇到“能编译能链接、一运行就崩”的情况排查方向就该转到函数调用约定和参数传递时序上而不是继续怀疑 VS 2019 的工程配置。把这里的machine换成subsystem还能顺带确认目标文件是控制台程序还是 GUI 程序这条命令在排查连接问题时基本够用。本文还有配套的精品资源点击获取
返回列表