简介面向C/MFC开发者的VS2019动态链接库封装与调用示例包聚焦MFC常规库与扩展库两种DLL形态并给出非模态调用场景下的完整工程对照适合需要理解DLL接口设计、模块复用与调用端集成的中初级Windows桌面开发者。压缩包内共224个文件约328.67MB涵盖h/cpp源码、sln/vcxproj工程、dll/lib/exp导入库、rc/res资源脚本以及pdb、tlog、obj等编译调试中间文件较完整地保留了从工程配置到生成产物的目录脉络。已有1039人学习下载。可对照MFC扩展DLL的AFX_EXT_CLASS导出与常规DLL接口封装观察非模态对话框、自绘按钮与静态控件等模块在调用端的组织方式并借助def、lib及日志文件排查链接、加载和运行时依赖问题对梳理MFC DLL的静态/动态链接差异和工程化复用有参考价值。1. 从一次“对话框模板找不到”说起VS2019 MFC DLL 共享库例程到底解决什么主程序点一个按钮弹出一个非模态配置窗窗口在 DLL 里规则校验也在 DLL 里Debug 下一切正常Release 换台机器就报“对话框模板找不到”。这不是玄学是 VS2019 下 MFC DLL 共享动态链接库最常见的模块状态与生命周期问题。这套例程把 MFC 常规 DLL共享 MFC DLL、MFC 扩展 DLL、无界面规则库和非模态调用拆成独立工程界面 DLL 只负责窗口和资源规则库只负责纯函数校验主程序通过导出接口创建和销毁非模态窗口。适合正在做插件化配置界面、想把 MFC 代码从 EXE 拆出去、又不想被资源句柄和 CRT 混用反复翻车的 C/MFC 开发者。下面按工程参数、导出接口、非模态生命周期、扩展 DLL 边界和排查顺序拆开讲。2. 在 VS2019 里建 MFC 常规 DLL共享 MFC 的工程参数、导出名和规则库接口2.1 选“MFC 常规 DLL”还是“MFC 扩展 DLL”先把边界定死VS2019 新建 MFC DLL 工程时向导会给三个选项使用共享 MFC DLL 的常规 DLL、使用静态 MFC 的常规 DLL、MFC 扩展 DLL。名字很像边界差别很大。常规 DLL 的定位是“导出一组 C 风格接口内部可以用 MFC 做窗口、资源、字符串处理”调用方不一定是 MFC 程序扩展 DLL 的定位是“把 MFC 派生类直接导出给另一个 MFC 程序用”调用方必须已经完成 MFC 初始化并且加载时要把 DLL 挂进CDynLinkLibrary链。规则库这种只做判断、不弹窗的模块用常规 DLL 导出纯函数最稳配置窗口如果只是 DLL 内部创建、外部只拿HWND也用常规 DLL只有确实要把CDialogEx派生类、CView派生类暴露给主程序new的时候才上扩展 DLL。选共享 MFC DLL 而不是静态链接 MFC主要看两点。第一多个模块共享同一份 MFC 运行库CString、CObject、异常和内存管理走同一套堆跨 DLL 传 MFC 对象时少一层“谁分配谁释放”的扯皮。第二主程序、界面 DLL、规则库 DLL 的运行时库容易统一到/MDd和/MD不会出现 Debug 模块把CString传给 Release 模块这种血泪经验。代价是目标机器要有对应版本的 MFC 运行库交付时要么带运行库安装包要么在工程里明确依赖。静态 MFC 的常规 DLL 不是不能用但它更适合“整个 DLL 自包含、不跨边界传 C 对象”的场景一旦导出类或者跨 DLL 传std::string释放方和分配方对不上后面就要吃后悔药。这套例程里MfcRegularDll是共享 MFC 的常规 DLL负责非模态配置窗口和窗口资源MfcRuleDll也是常规 DLL但只导出规则校验函数不创建任何窗口MfcExtDll是扩展 DLL用AFX_EXT_CLASS导出一个对话框类用来对比常规 DLL 和扩展 DLL 的加载方式。三个工程的字符集统一 Unicode平台工具集统一 v142C 语言标准保持一致避免/std:c17和默认标准混用导致头文件里内联变量、inline函数行为不一致。实际做插件时我一般先把“谁创建窗口、谁持有对象、谁负责销毁”写成三句话再决定用哪种 DLL这三句话没定工程建出来也会在卸载顺序上翻车。2.2 建工程与关键配置Use of MFC、运行时库、导出名新建工程时选“动态链接库DLL”不要选“控制台应用”再手动改配置类型在 MFC DLL 向导里选“使用共享 MFC DLL 的常规 DLL”让向导把pch.h、framework.h、CWinApp派生类和资源脚本生成好。生成后先改三处工程属性再写代码。第一处是“常规 → 使用 MFC”选“在共享 DLL 中使用 MFC”第二处是“C/C → 代码生成 → 运行时库”Debug 选“多线程调试 DLL (/MDd)”Release 选“多线程 DLL (/MD)”第三处是“高级 → 字符集”选“使用 Unicode 字符集”。主程序MfcHost也要做同样设置不能主程序用/MTd、DLL 用/MDd否则new和delete跨模块时堆不一致表现就是 Release 换机器必崩。导出名要提前定不要等调用方GetProcAddress返回空再回头查。C 函数默认会名称修饰__stdcall在 x86 下还会带和参数总字节数如果调用方用GetProcAddress(hDll, CreateConfigModeless)而 DLL 实际导出的是_CreateConfigModeless4就会拿到空指针。常见做法是加一个.def文件把对外名称固定住x86 构建时写修饰名映射x64 构建时另给一份不带修饰的导出列表。下面这份.def对应三个导出函数创建非模态窗口、销毁非模态窗口、执行规则校验。; MfcRegularDll.def LIBRARY MfcRegularDll EXPORTS ; x86 下 __stdcall 导出名带 和参数字节数用别名固定对外名称 CreateConfigModeless_CreateConfigModeless4 DestroyConfigModeless_DestroyConfigModeless4 RuleCheckInt_RuleCheckInt12这段.def的逻辑是链接器按等号左边的名字对外导出调用方GetProcAddress只用左边这个名字等号右边是编译器实际生成的修饰名。参数说明CreateConfigModeless和DestroyConfigModeless各有一个HWND参数x86 下是 4 字节所以修饰名后缀是4RuleCheckInt有三个参数int、int、int*共 12 字节后缀是12。如果工程只出 x64x64 没有__stdcall名称修饰.def里直接写CreateConfigModeless等名字即可不要把 x86 的4抄过去。每次改完导出先用dumpbin /exports MfcRegularDll.dll看一眼实际导出表再写调用端比事后猜名字省时间。头文件里把导出宏和extern C写清楚调用方包含同一份头文件时可以走隐式链接不想依赖.lib就用LoadLibrary加GetProcAddress。下面这份头文件同时服务两种调用方式MFCREGULARDLL_EXPORTS只在 DLL 工程里定义主程序包含时自动变成dllimport。// MfcRegularDll.h #pragma once #ifdef MFCREGULARDLL_EXPORTS #define MFCREGULAR_API __declspec(dllexport) #else #define MFCREGULAR_API __declspec(dllimport) #endif extern C { MFCREGULAR_API HWND __stdcall CreateConfigModeless(HWND hParent); MFCREGULAR_API BOOL __stdcall DestroyConfigModeless(HWND hDlg); MFCREGULAR_API int __stdcall RuleCheckInt(int ruleId, int value, int* outLevel); }这段头文件的逻辑是用extern C禁止 C 名称修饰用__declspec(dllexport/dllimport)让同一份声明在 DLL 和调用方都能用。参数说明hParent是父窗口句柄不能传nullptr否则非模态窗口没有 owner主窗口最小化时它不会跟着隐藏hDlg是创建时返回的窗口句柄销毁时必须和创建时一致不能拿子控件句柄来传outLevel是输出参数调用方负责分配intDLL 只写值不负责释放。这里刻意不返回CString、std::string或自定义类指针就是为了避开跨 DLL 堆释放的问题。注意导出函数声明和定义里的调用约定必须一致头文件写__stdcall实现里也要写__stdcall只在头文件写、实现里漏写链接器不会报错但运行时栈会乱。2.3 规则库例程把业务判断压成纯函数不跨边界传 C 对象规则库例程的价值不是“再写一个 DLL”而是演示怎么把一组判断规则从界面代码里剥出来做成无状态、可单独测试的导出函数。常见做法是输入用基本类型或调用方分配的缓冲区输出用返回码加输出参数DLL 内部可以读配置、查表、做边界判断但不创建窗口、不弹消息框、不依赖主程序的全局变量。这样规则库既能被 MFC 主程序调用也能被单元测试程序、命令行工具调用后面要换界面框架也不用重写规则。下面这段实现只保留两条示例规则重点看参数检查和返回码约定。// MfcRuleDll.cpp #include pch.h #include MfcRegularDll.h extern C MFCREGULAR_API int __stdcall RuleCheckInt(int ruleId, int value, int* outLevel) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); if (outLevel nullptr) return -1; // 参数错误 switch (ruleId) { case 1: // 示例规则超过上限给高等级 *outLevel (value 80) ? 2 : 0; return 0; case 2: // 示例规则负值直接拦截 *outLevel (value 0) ? 3 : 1; return 0; default: return -2; // 未知规则 } }这段代码的逻辑是先切换模块状态再检查输出指针最后按ruleId分支写outLevel。参数说明ruleId由调用方传入1 和 2 是示例规则编号真实项目里可以换成枚举值但枚举不要跨 DLL 边界直接传调用方和 DLL 各自包含同一份头文件即可value是待判断的整数outLevel是输出等级调用方必须先分配好int再传地址。返回码约定为 0 成功、-1 参数错误、-2 未知规则调用方先看返回码再看outLevel不要反过来。AFX_MANAGE_STATE在这里不是可选项即使规则库不弹窗只要 DLL 内部可能访问字符串资源或 MFC 状态导出函数入口就要加如果规则库完全不用 MFC也可以把工程建成纯 Win32 DLL减少依赖。规则库接口还有两个常见坑。第一不要返回 DLL 内部new出来的char*让调用方delete除非同时导出FreeRuleResult之类的释放函数并且调用方严格配对更稳的做法是调用方传缓冲区进去DLL 只填充缓冲区长度也作为参数传进来。第二不要用CString作为导出参数除非调用方和 DLL 都确认使用共享 MFC DLL并且异常设置一致一旦有一方静态链接 MFCCString的内部指针跨模块释放就会出问题。规则库这种模块接口越像 C 越省事后面换编译器版本、换调用方语言都容易。3. 非模态调用不翻车从 DLL本文还有配套的精品资源点击获取