c++ QT之动态库加载问题

问题一、extern c _declspec(dllexport)

一、什么是名字粉碎 Name‑Mangling(名称修饰)

C 语言:函数名是什么,编译后符号名就是什么

void foo(int a);

// 符号:foo

C++支持重载、命名空间、类成员函数、const修饰符。仅仅靠函数名已经区分不开。

void foo(int);

void foo(double);

namespace NS{ void foo(int); }

class A{ void foo(int) const; };

所以 C++编译器会把完整信息编码进符号名,这个编码过程就是名字粉碎(mangling)。

Windows MSVC(VS2019)

void foo(int) → ?foo@@YAXH@Z

void foo(double) → ?foo@@YAXN@Z

注意两点极其重要:

  1. 每个编译器厂商 mangling 规则不一样:MSVC一套,GCC一套,Clang一套;

  2. 同一个编译器,不同版本,mangling规则理论上尽量兼容,但不保证永久兼容(ABI不稳定根源之一)。

二、extern "C"到底做了什么?

extern "C" void foo(int);

一句话:告诉C++编译器:这个符号不要做C++名字粉碎,保留C风格原始符号名

extern "C" void foo(int) → 符号直接就是 foo。

extern "C" 的用途(Windows原生DLL时代)

原生Windows插件机制是:宿主调用 GetProcAddress(hDll, "foo"),按字符串名字查找导出符号。

• 如果不加 extern C:真实导出符号是 ?foo@@YAXH@Z;你写 GetProcAddress(hdll,"foo") → 找不到,返回NULL。

• 加上 extern C:导出符号就是 foo;GetProcAddress("foo")可以拿到地址。

✅这就是传统Windows C插件DLL必须写 extern "C" 的根本原因。

extern "C" 的局限,也是很多人的误区

  1. extern C 只能修饰普通全局函数,不能导出C++类、成员函数、重载函数。

也就是说:靠 extern C 这条路,无法直接把一整个C++类导出给外部调用。

这是一个分水岭。绝大多数人在这里卡住:既然 extern C 不能导出C++类,那Qt插件是怎么做到导出C++接口的?

  1. extern C不会改变函数内部实现,不会改变调用约定,只是改变符号名字。

三、Qt插件为什么不需要 extern "C",就可以导出C++接口?

先破除一个流传很广的错误说法:

❌错误观点:Qt修改了Windows导出机制,原生支持导出C++类。

真相不是这样。我们拆解Qt插件两条宏:

Q_DECLARE_INTERFACE(IEntityPlugin, "com.company.IEntityPlugin/1.0")

Q_PLUGIN_METADATA(IID "com.company.IEntityPlugin/1.0" FILE "plugin.json")

Qt插件 DLL里面到底导出了什么?

Qt插件dll只导出了唯一一个C风格全局函数:qt_plugin_instance(这个函数Qt内部自己加了extern C!)

注意:插件里面的C++类本身没有导出! Windows导出表里面看不到插件类的符号。

执行流程:

  1. QPluginLoader::load() → Windows底层调用LoadLibrary加载dll;

  2. QPluginLoader内部 GetProcAddress(handle, "qt_plugin_instance")

◦ 这是一个固定名字、extern‑C修饰的全局函数,所以GetProcAddress可以找到;

  1. 调用这个函数,函数内部 new 出来插件类实例,返回一个指向纯虚接口(IEntityPlugin)的指针;

  2. 宿主拿到接口指针,调用虚函数。

关键点来了:宿主根本不需要知道插件具体类的名字,不需要找到类成员函数符号。

宿主只认识抽象接口(纯虚类),调用通过虚函数表 vtable分发,不是通过符号名查找成员函数!

✅一句话总结Qt插件魔法:

不是导出C++类,而是导出一个extern‑C全局工厂函数;工厂返回一个纯虚接口指针;所有功能调用走虚表分发,绕开了C++名字粉碎问题。

这就是为什么不需要 extern C修饰每一个成员函数。

但是这里有一条铁律(工控项目最容易踩的大坑)

宿主和插件必须使用同一个版本Qt,同一个编译器,同一个编译选项编译!

为什么?因为虚函数表布局(vtable layout)属于ABI。MSVC不同版本,Qt不同版本,虚表布局如果变化,调用虚函数直接崩溃。

这个就是Qt插件最大痛点,它解决了名字粉碎,但没有解决C++ ABI不稳定。

这也是很多大型军工工控项目,宁愿自己手写C风格接口插件,也不用Qt原生插件系统的原因。

四、对比两条插件路线(面试经常追问)

路线A:原生Windows插件,extern‑C导出普通函数

extern "C" __declspec(dllexport) int add(int a,int b);

宿主:LoadLibrary → GetProcAddress("add")。

优点:最简单;缺点,不能直接导出类。

路线B:Qt插件路线(工厂函数 + 纯虚接口 + vtable)

dll只导出一个extern‑C工厂函数,返回接口指针,业务函数走虚表。

优点:可以提供完整面向对象接口;缺点:强ABI依赖,Qt版本、编译器版本必须统一。

路线C:PIMPL + extern‑C(很多商用工控SDK的方案)

对外只暴露不完整类型(前置声明),所有操作通过extern‑C全局函数操作句柄。完全规避C++ ABI不稳定。很多DDS厂商C++ SDK就是这么交付的。这条线正好串起你前面学习的PIMPL。

五、面试口述版,直接背诵

C++为了支持重载、命名空间引入名字粉碎,编译器给函数生成编码后的符号名;extern "C"的作用就是关闭名字粉碎,导出C风格原始符号名,主要用于Windows下GetProcAddress动态加载全局函数;但是extern C不能导出C++类。

Qt插件并没有直接导出C++类,它在dll内部自动生成一个extern‑C修饰的全局工厂函数并导出;宿主加载dll后拿到这个工厂函数,创建对象返回纯虚接口指针,后续全部调用通过虚函数表分发,绕开了名字粉碎。代价是插件和宿主必须保持ABI完全一致,Qt版本和编译器版本要统一。

如果你愿意,下一步我们可以继续深挖:

1)为什么C++标准没有统一ABI?

2)基于PIMPL+extern‑C,手写一套不依赖Qt的C++插件系统完整demo(工控里面最推荐的一种方案)。

问题二、QT的Q_DECL_EXPORT原生dll加载以及plugin库加载

Q_DECL_EXPORT 到底是什么,为什么Qt原生插件不用它导出插件类

先开门见山,很多开发者在这里混淆了两套完全不同的DLL方案:

Q_DECL_EXPORT:用于常规Qt共享库(SDK动态库,隐式链接,h+lib+dll)

Q_PLUGIN_METADATA:用于Qt原生插件库(运行时QPluginLoader加载)

两者解决的不是同一个问题,Qt原生插件不使用Q_DECL_EXPORT导出插件类,这是最大误区。

1、Q_DECL_EXPORT底层是什么?

// qglobal.h里的真实定义 Windows平台

#define Q_DECL_EXPORT __declspec(dllexport)

#define Q_DECL_IMPORT __declspec(dllimport)

它只是跨平台封装了MSVC的__declspec(dllexport),作用:把这个C++类/函数加入DLL导出表。

举一个普通Qt共享库(SDK库)写法:

#if defined(MYDLL_LIBRARY)

define MYDLL_EXPORT Q_DECL_EXPORT

#else

define MYDLL_EXPORT Q_DECL_IMPORT

#endif

class MYDLL_EXPORT MyClass

{

public:

void test();

};

✅特点

  1. 这个类全部成员函数都会以粉碎后的C++符号名(mangled name)导出到DLL导出表,例如?test@MyClass@@QEAAXXZ;

  2. 宿主编译阶段include头文件 + 链接import lib,隐式链接调用这个类;

  3. 它不能用于LoadLibrary + GetProcAddress动态查找这个类!导出的是粉碎后的符号名;GetProcAddress("MyClass")根本找不到;

划重点:Q_DECL_EXPORT解决的是编译期链接(隐式链接SDK库),不是运行时动态加载(插件)。

这就是常规Qt动态库SDK交付形态(h + import‑lib + dll),就是你之前问的SDK。

⚠️非常重要的现实问题:就算你用Q_DECL_EXPORT导出完整C++类给隐式链接使用,依然存在ABI不稳定风险,宿主和dll必须同一个编译器、同一个Qt版本编译。

2、那Qt原生插件DLL里,插件类前面为什么不加Q_DECL_EXPORT?

回顾Qt插件流程:

class MyPlugin : public QObject, public IEntityPlugin

{

Q_OBJECT

Q_INTERFACES(IEntityPlugin)

Q_PLUGIN_METADATA(IID "com.xxx.IEntityPlugin/1.0")

public:

void run() override;

};

很多人疑惑:类没有Q_DECL_EXPORT,那它怎么暴露给外部?

真相:

  1. MyPlugin这个类本身根本没有导出!Windows导出表看不到这个类任何符号;

  2. moc读取Q_PLUGIN_METADATA这个宏,自动生成一个extern "C"全局工厂函数 qt_plugin_instance(),只有这个工厂函数被导出(moc内部自己添加导出标记,不需要你写Q_DECL_EXPORT);

  3. QPluginLoader加载dll以后,GetProcAddress拿到qt_plugin_instance这个C函数地址;调用这个函数new出插件实例,返回抽象接口指针;

  4. 宿主后续调用全部走虚函数表分发,不查找任何类成员符号。

一句话:

Q_DECL_EXPORT导出类,是编译期链接用的SDK库方案;

Q_PLUGIN_METADATA生成extern‑C工厂函数,是运行时动态加载插件方案。两条路线。

这里衍生出一个高频面试追问

问:我能不能给Qt插件类加上Q_DECL_EXPORT?

答:语法允许,但是毫无意义,多余。

• 即便导出了MyClass,导出符号是粉碎后的C++符号;QPluginLoader不会去查找这个类符号;

• 反而白白扩大dll导出表;工控项目一般不加。

3、横向对比两条路线(面试口述版)

路线A:Q_DECL_EXPORT共享库(SDK动态库)

• 使用方式:隐式链接,头文件+import lib+dll;

• 加载时机:程序启动时操作系统加载dll;

• 适用场景:公共基础模块,开发期就确定依赖;

• 缺点:不能做运行时扫描文件夹加载模块,做不了插件系统;

• 符号状态:C++ mangled名字导出。

路线B:Q_PLUGIN_METADATA插件库

• 使用方式:QPluginLoader运行时动态加载;

• 加载时机:程序运行过程中,按需加载dll;

• 适用场景:工控插件架构,可扩展模块;

• 实现机制:moc生成extern‑C工厂函数,导出这一个函数;返回纯虚接口指针;调用走vtable;

• 约束:ABI必须一致(编译器版本、Qt版本一致)。

拓展第三种路线(商用工控SDK):QLibrary + 手写extern‑C工厂函数,不依赖Qt元对象系统,配合PIMPL模式;这个是军工项目里面最稳定的插件写法,规避Qt版本强绑定。

4、面试标准答案,直接背下来

Q_DECL_EXPORT是Qt对__declspec(dllexport)跨平台封装,用来导出C++类和符号,主要用于普通共享库(SDK动态库),依赖头文件加导入库做编译期隐式链接;导出的C++符号带有名字粉碎信息,不能直接通过GetProcAddress动态查找。

Qt原生插件系统不使用Q_DECL_EXPORT导出插件类;Q_PLUGIN_METADATA交由moc自动生成一个extern‑C修饰的全局工厂函数并导出,宿主拿到工厂函数创建对象后,通过纯虚接口的虚函数表调用功能,以此实现运行时插件加载。

下一步,我们可以继续展开:手写一套无Qt依赖的插件系统,extern‑C工厂函数 + PIMPL模式实现工控插件。这个是面试加分项。

相关推荐
(Charon)1 小时前
【C++】定时器进阶:使用最小堆管理定时任务
c++·算法
hansang_IR2 小时前
【题解】 [省选联考 2021 A/B 卷] 卡牌游戏
c++·算法
lzx_0022 小时前
C++11(一)
开发语言·c++·算法
小七在进步4 小时前
C++入门(2)
java·jvm·c++
en.en..4 小时前
C语言核心解析:#define与typedef本质区别
开发语言·c++·算法
码匠许师傅4 小时前
【设计模式精讲】26.策略模式(Strategy)
c++·设计模式·策略模式·uml
果果燕5 小时前
实习笔记(六)主机按钮状态上报完整流程
开发语言·c++·php
是个西兰花6 小时前
网络基础1
linux·网络·c++·智能路由器
Quz6 小时前
QML Loader:动态创建控件与延迟加载
qt