如何打破C++项目中Main.exe与Framework.dll的循环依赖
打破C++模块循环依赖的落地方案
核心目标:保留Main.exe对Framework生命周期类的使用体验,构建Main.exe时无需链接Framework.lib,彻底解除二者构建时的循环依赖。
推荐方案:抽象公共接口 + 运行时动态加载
这个方案改动量极小,兼容性最好,完全不影响现有插件模块的逻辑。
实现思路
把Main.exe依赖的Framework生命周期能力抽成纯虚抽象接口,放在公共头文件中供两个模块引用;Framework.dll内部继承该接口实现原有逻辑,对外导出C风格的工厂/销毁函数;Main.exe通过Windows API运行时动态加载Framework.dll,拿到工厂函数创建接口实例使用,全程不需要链接Framework的导入库。
具体代码实现
- 新增公共头文件
IFramework.h,由Main.exe和Framework.dll共同包含,无任何模块绑定:
#pragma once // 纯虚接口,只定义能力约定,不包含实现 class IFramework { public: virtual ~IFramework() = default; virtual void Init() = 0; virtual void Begin() = 0; virtual void End() = 0; virtual void Shutdown() = 0; }; // 约定导出函数的签名,用extern "C"避免C++名字修饰问题 extern "C" { using FnCreateFramework = IFramework* (*)(); using FnDestroyFramework = void (*)(IFramework*); }
- 修改Framework.dll代码,原有
Framework类逻辑几乎不用改动,只要继承接口、导出工厂函数即可:
#include "IFramework.h" // 原有Framework类全部逻辑保留,仅添加接口继承 class Framework : public IFramework { public: void Init() override { /* 原有Init逻辑不变 */ } void Begin() override { /* 原有Begin逻辑不变 */ } void End() override { /* 原有End逻辑不变 */ } void Shutdown() override { /* 原有Shutdown逻辑不变 */ } private: // 原有所有成员变量、私有方法完全保留 }; // 导出C风格工厂函数,供外部动态加载调用 extern "C" __declspec(dllexport) IFramework* CreateFramework() { return new Framework(); } extern "C" __declspec(dllexport) void DestroyFramework(IFramework* inst) { delete inst; }
注意:对象的创建、销毁逻辑全部放在Framework.dll内部,不会出现跨模块堆分配释放不匹配的问题。
- 修改Main.exe代码,移除对
Framework.hpp的引用和Framework.lib的链接,改为运行时动态加载:
#include "IFramework.h" #include <windows.h> // 可以封装RAII持有类,对齐原有栈对象的使用体验 struct FrameworkGuard { HMODULE hFwDll = nullptr; IFramework* fwInst = nullptr; FnDestroyFramework destroyFn = nullptr; FrameworkGuard() { hFwDll = LoadLibraryW(L"Framework.dll"); if (!hFwDll) { /* 加载失败处理,比如弹提示退出 */ } auto createFn = reinterpret_cast<FnCreateFramework>( GetProcAddress(hFwDll, "CreateFramework") ); destroyFn = reinterpret_cast<FnDestroyFramework>( GetProcAddress(hFwDll, "DestroyFramework") ); if (!createFn || !destroyFn) { /* 导出函数缺失处理 */ } fwInst = createFn(); } ~FrameworkGuard() { if (fwInst && destroyFn) destroyFn(fwInst); if (hFwDll) FreeLibrary(hFwDll); } IFramework* operator->() const noexcept { return fwInst; } // 禁止拷贝,避免重复释放 FrameworkGuard(const FrameworkGuard&) = delete; FrameworkGuard& operator=(const FrameworkGuard&) = delete; }; int main() { FrameworkGuard fw; fw->Init(); while (Quit() == false) { fw->Begin(); DoWork(); fw->End(); MoreWork(); } fw->Shutdown(); return 0; }
方案优势
- 彻底解除构建时循环依赖:构建顺序变为「编译Main.exe生成
Main.lib→ 链接Main.lib编译Framework.dll → 生成最终Main.exe」,全程不需要Framework.lib参与Main.exe的链接过程。 - 改动量极小:Framework类的核心逻辑、所有插件模块的原有代码完全不需要修改,不影响现有业务。
- 使用体验对齐原有代码:通过RAII封装后,上层调用逻辑和原本栈上创建
Framework对象的写法几乎一致,没有额外的使用成本。 - 兼容性强:C风格导出函数不会受C++名字修饰规则影响,不会出现不同编译选项下找不到导出符号的问题。
其他可选方案(不推荐)
- 拆分Framework.dll的生命周期类为独立静态库:把Main.exe用到的Framework类单独抽成小静态库同时链接给Main和Framework,但是会造成代码冗余,且如果这个类依赖Framework内部其他逻辑,拆分成本极高。
- 使用延迟加载DLL:本质还是需要链接
Framework.lib,无法解决构建时的循环依赖问题,需要额外做lib的手工导入导出处理,复杂度远高于动态加载方案。
内容的提问来源于stack exchange,提问作者Dess
相关产品推荐
相关产品推荐

