当一个工程从单体架构(Monolithic)演进为微内核插件化架构(Microkernel)时,代码的解耦只完成了一半,另一半的重头戏在于构建系统(Build System)的彻底改造。
本文将结合 QTTest 音视频框架的实战,深度剖析在 C++ 插件化编程中,CMake 究竟需要做哪些关键的改动,以及如何实现"构建即部署"的自动化流水线。
一、 为什么单体 CMake 无法支撑插件化?
在单体架构下,我们通常只需要一个 CMakeLists.txt,把所有的 .cpp 文件揉在一起,编译出一个庞大的 .exe 或静态库。
但在微内核架构下,我们面临三个致命问题:
- 符号不可见 :插件 DLL 是在运行时被动态加载的,如果核心库不导出自己的函数符号(如
Logger或VideoEngine),插件加载时就会报错找不到函数。 - 重复定义冲突 :插件必须继承核心的抽象接口(如
IVideoEncode.h)。如果 CMake 配置不当,接口文件会被编译两次(一次在核心,一次在插件),导致链接时的多重定义(Multiple Definition)灾难。 - 部署难题 :CMake 默认会将不同模块编译到各自嵌套的
build/目录下。主程序启动时,根本找不到这些散落天涯的插件 DLL。
为了解决这些问题,我们需要对 CMake 进行**"四大改造"**。
二、 CMake 的四大核心改造
改造 1:核心库(Core)的动态化与符号导出
核心库不能再是隐藏在背后的静态库,它必须变成动态库(SHARED),并向外暴露所有的基础 API,供插件调用。
在 src/core/CMakeLists.txt 中:
cmake
# 1. 将核心引擎编译为动态库 (Shared Library)
add_library(Core SHARED ${CORE_SOURCES})
# 2. 【极其关键】Windows 平台的魔法开关
# 开启后,CMake 会自动将 Core 库中的类和函数符号导出 (dllexport)
# 这样插件在链接 Core.lib 时,就能在运行时找到对应的内存地址
set_target_properties(Core PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
改造 2:插件的独立编译与反向链接
以前所有的具体实现(如 Nvenc 编码器)都在主 CMake 里。现在,它们被剥离到了 src/plugins/ 各个子目录下,并拥有自己独立的 CMakeLists.txt。
在 src/plugins/encoder/CMakeLists.txt 中:
cmake
# 1. 插件本身也是一个独立的动态库
add_library(plugin_encoder SHARED ${PLUGIN_SOURCES})
# 2. 引入主工程的头文件路径,确保插件能找到 IVideoEncode.h 等接口定义
target_include_directories(plugin_encoder PRIVATE ${CMAKE_SOURCE_DIR}/src)
# 3. 插件必须反向链接到 Core 库,以使用 Core 提供的日志、引擎基础功能
target_link_libraries(plugin_encoder PRIVATE Core)
改造 3:源文件去重(防止 ODR 违规)
由于插件工程和核心工程共享了接口头文件,如果使用了 file(GLOB ...) 扫描源文件,极易将接口的 .cpp 文件重复包含到插件的编译列表中,从而违反 C++ 的单一定义规则(ODR)。
在插件的 CMakeLists.txt 中,需要进行精准的剔除:
cmake
# 扫描当前插件目录下的所有源码
file(GLOB_RECURSE PLUGIN_SOURCES "*.cpp" "*.h")
# 【防坑指南】从编译列表中移除属于 Core 的基础接口实现文件
list(REMOVE_ITEM PLUGIN_SOURCES
"${CMAKE_SOURCE_DIR}/src/plugins/encoder/IVideoEncode.cpp"
"${CMAKE_SOURCE_DIR}/src/plugins/encoder/EncoderCommon.h"
)
改造 4:自动化部署(Post-Build 后置构建)
这是插件化工程里最能提升幸福感的一步。我们要让 CMake 在编译完成后,自动把插件 DLL 像装配零件一样,搬运到主程序 CoreService.exe 的运行目录中。
在插件的 CMakeLists.txt 末尾,添加 POST_BUILD 脚本:
cmake
# 第一步:确保在主程序 (CoreService) 输出目录下,存在 plugins 文件夹
add_custom_command(TARGET plugin_encoder POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:CoreService>/plugins
)
# 第二步:将刚刚编译出来的 plugin_encoder.dll,自动拷贝到 plugins 文件夹内
add_custom_command(TARGET plugin_encoder POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:plugin_encoder> $<TARGET_FILE_DIR:CoreService>/plugins/
)
三、 总结:构建流水线的全貌
经过这四大改造,整个 QTTest 工程实现了一条工业级的自动化构建流水线:
- 点击 Build (生成)。
- CMake 首先编译出
Core.dll,并生成包含导出符号的Core.lib。 - CMake 接着并行编译各个插件模块(如
plugin_encoder.dll、plugin_input.dll),在编译时链接Core.lib。 - 编译完成后,触发
POST_BUILD钩子。 - 所有插件的
.dll文件被自动汇聚,整齐地归档到CoreService.exe同级的plugins/目录中。 - 运行主程序 ,
PluginManager瞬间扫描到所有插件并动态加载,系统完美运行。
掌握了这一套 CMake 配置,你不仅解耦了代码,更解耦了团队的协作流。未来即使有 10 个工程师并行开发不同的插件,也不会再发生任何的构建冲突。