插件反射无法依赖std::any和虚函数表,因C++ RTTI不跨动态库边界,必须通过显式注册函数(extern "C")由主程序dlsym/GetProcAddress获取并调用,将类型元信息填入主程序单例PluginRegistry。

为什么 std::any 和虚函数表不能直接支撑插件反射
因为插件在编译期不可见,类型信息必须运行时重建。C++ 原生 RTTI 不跨动态库边界生效——typeid 在主程序和 .so/.dll 中对同一类会生成不同实例,dynamic_cast 跨库失败是常态。你不能靠继承一个基类 + virtual 函数就让插件自动“被发现”。
真正可行的路径只有一条:插件导出一个**显式注册函数指针**,主程序用 dlsym(Linux/macOS)或 GetProcAddress(Windows)手动取出来调用。这个函数负责把插件内的类型元信息(类名、构造器、字段列表等)填入主程序维护的全局注册表。
- 每个插件必须定义且导出
extern "C" void register_plugin(PluginRegistry* reg),避免 C++ 名字修饰干扰 - 主程序的
PluginRegistry必须是单例,且接口设计为纯虚类(如register_type(const char*, CreatorFn)),实现类驻留在主程序中 - 禁止在插件里 new 主程序定义的类(内存分配器可能不一致),所有对象生命周期由主程序统一管理
如何用 std::unordered_map + std::function 实现类型工厂注册表
注册表本质是个字符串到构造器的映射,但要注意:构造器返回的是 std::unique_ptr<base>,而 Base 是主程序定义的抽象接口;插件里的具体类必须公有继承它,且构造函数无参(或仅接受 const std::string& config 等主程序可控参数)。
示例注册逻辑(主程序侧):
立即学习“C++免费学习笔记(深入)”;
class PluginRegistry {
public:
using CreatorFn = std::function<std::unique_ptr<Base>()>;
void register_type(const char* name, CreatorFn fn) {
creators_[name] = std::move(fn);
}
std::unique_ptr<Base> create(const char* name) {
auto it = creators_.find(name);
return it != creators_.end() ? it->second() : nullptr;
}
private:
std::unordered_map<std::string, CreatorFn> creators_;
};-
CreatorFn不能捕获局部变量(插件卸载后会 dangling),只能捕获静态数据或通过std::bind绑定插件内静态函数 - 注册时建议加
assert(!creators_.count(name))防止插件重名覆盖 - 若需支持带参数构造,不要用可变模板参数包(跨库 ABI 不稳定),改用统一的
std::map<std::string, std::string>配置传入
dlopen 加载插件时必须传 RTLD_LAZY | RTLD_GLOBAL
只用 RTLD_LAZY 会导致符号解析延迟到首次调用,而 dlsym 查不到未解析的符号;缺 RTLD_GLOBAL 则插件内部依赖的其他动态库(比如它链接了 libjson.so)无法被后续加载的插件复用,引发 undefined symbol 错误。
典型加载流程:
void* handle = dlopen("./plugin_example.so", RTLD_LAZY | RTLD_GLOBAL);
if (!handle) { fprintf(stderr, "%s\n", dlerror()); return; }
auto reg_fn = reinterpret_cast<void(*)(PluginRegistry*)>(dlsym(handle, "register_plugin"));
if (!reg_fn) { fprintf(stderr, "symbol 'register_plugin' not found\n"); dlclose(handle); return; }
reg_fn(&g_registry);
// 注意:handle 不能 close —— 否则函数指针失效,且静态数据丢失- Windows 下对应为
LoadLibraryEx+GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,否则FreeLibrary会提前释放 - 插件路径必须是绝对路径或
LD_LIBRARY_PATH可达路径,相对路径在工作目录切换后极易失败 - 调试时用
LD_DEBUG=libs,symbols查看符号是否真实导出(检查register_plugin是否在nm -D plugin.so输出中)
插件卸载前必须清空所有该插件创建的对象
动态库卸载(dlclose)后,其代码段、数据段全部回收,任何残留的函数指针或静态变量访问都会导致段错误。但 C++ 对象析构可能触发插件内的虚函数(比如自定义 ~Derived()),此时插件已不存在。
安全做法只有两种:
- 插件只提供无析构逻辑的 POD 类型,或所有资源交由主程序 RAII 管理(例如用
std::shared_ptr持有句柄,销毁回调在主程序中) - 主程序提供
unregister_type(const char*)接口,插件在卸载前主动调用,同时主程序遍历所有存活对象,强制reset()掉属于该插件类型的实例 - 永远不要在插件里注册全局对象(如
static std::vector),它们的析构时机不可控
最易忽略的一点:插件内使用了 std::thread 或 std::async?线程函数体必须在主程序中定义,插件只传参数——否则线程栈上还存着已卸载的代码地址。


















