
前言很多大型软件都采用插件化架构,例如 OBS Studio、浏览器、IDE、音视频编辑器和游戏引擎。宿主程序只负责核心流程,滤镜、编码器、数据导入器等扩展能力则由插件提供。本文通过一个最小但完整的 Windows C++ 示例,介绍插件系统的基本原理,并实现下面这条调用链:app_host.exe | | LoadLibrary("my_filter.dll") v my_filter.dll | | GetProcAddress +-- GetPluginName() +-- ProcessData(21) | v 返回结果 42,随后 FreeLibrary()示例代码只依赖 Windows API 和 CMake,适合用来理解动态链接库、导出符号、函数指针和 ABI 边界。一、什么是插件系统插件系统通常由两部分组成:Host(宿主):负责启动、发现插件、加载动态库并调用插件。Plugin(插件):以 DLL、SO 或 DYLIB 的形式存在,按照约定导出函数。宿主和插件不需要在构建时静态链接。只要双方遵守相同的二进制接口约定,宿主就可以在运行时加载插件。Windows、Linux 和 macOS 对应的核心 API 如下:操作WindowsLinux/macOS加载动态库LoadLibrarydlopen查找符号GetProcAddressdlsym卸载动态库FreeLibrarydlclose本文工程使用 Windows API 实现。二、为什么需要插件化1. 解耦核心程序和扩展功能宿主只依赖一套稳定接口,不需要知道每个插件的内部实现。增加新插件时,可以不修改宿主的核心业务代码。2. 支持独立发布插件可以独立编译和升级。只要接口保持兼容,发布一个新 DLL 通常不需要重新构建整个应用。3. 按需加载只有真正使用某项功能时才加载对应插件,可以降低启动成本和基础内存占用。4. 建立扩展生态第三方开发者可以根据公开的 SDK 开发插件,而不需要获得宿主程序的全部源码。5. 隔离业务变化不同设备、协议、编解码器或滤镜可以分别封装为插件,使高频变化的业务逻辑不会持续侵入核心模块。三、插件系统的核心原理1. DLL 本质上是什么DLL 是一种包含机器代码和导出符号的动态链接库。操作系统加载 DLL 时,会将其映射到当前进程的虚拟地址空间。需要注意:普通 DLL 插件和宿主运行在同一个进程中。插件可以直接访问宿主传入的内存,调用成本很低,但插件发生非法内存访问时也可能导致整个宿主崩溃。2. 导出符号宿主无法直接调用 DLL 中任意一个内部函数,只能查找 DLL 导出的符号。Windows 下可以通过__declspec(dllexport)导出函数:__declspec(dllexport)intProcessData(intinput);宿主通过符号名查询函数地址:FARPROC address=GetProcAddress(module,"ProcessData");然后将地址转换为匹配的函数指针并调用。3. 为什么使用extern "C"C++ 支持函数重载,因此编译器会对函数名执行 Name Mangling(名字修饰)。不同编译器生成的符号名可能不同。例如,源码中的函数:intProcessData(intinput);经过 C++ 名字修饰后,DLL 中的实际符号可能不再是简单的ProcessData。使用extern "C"可以关闭 C++ 名字修饰:extern"C"{__declspec(dllexport)intProcessData(intinput);}``` 这样宿主就能通过固定字符串 `"ProcessData"` 查找函数。 ###4.C ABI 能解决什么 C ABI 边界通常比直接导出 C++类更稳定,原因包括:-导出符号名称更容易固定。--避免暴露虚函数表、继承布局等编译器细节。--避免直接暴露 `std::string`、`std::vector` 等 STL 类型。--其他语言可以通过 C FFI 接入。 不过,`extern"C"` 只解决名字修饰问题,并不自动保证所有场景下的二进制兼容。双方仍需统一:-CPU 架构,例如都使用 x64。--调用约定。--基础类型的大小和结构体对齐。--接口版本。--内存的申请和释放责任。--编译选项及运行库约束。 因此更准确的说法是:**C ABI 可以显著缩小兼容性风险,但不能代替完整的 ABI 设计。**---## 四、示例工程结构 ```text Plugin_System/|--CMakeLists.txt|--include/|`--PluginAPI.h|--src/||--host/