
从C++到工业级:Aether项目精讲 · 第3篇
一、理念是虚的,代码是实的
上一篇拆了插件架构的设计理念——为什么用插件、什么时候用、架构怎么搭。
但理念再漂亮,不落地就是空中楼阁。
这篇直接打开 Aether 的 ExtensionSystem 源码,一行行拆给你看:
PluginManager 怎么做到全局唯一?对象池怎么管理?插件的生命周期谁在驱动?依赖解析怎么处理循环引用?
看完这篇,你能自己写一个工业级的插件管理器。
二、PluginManager:全局唯一的"插件总管"
先看入口。整个插件系统的老大是谁?
// pluginmanager.h · 简化核心
class PluginManager : public QObject
{
Q_OBJECT
public:
static PluginManager *instance(); // ← 全局单例
// 对象池操作
static void addObject(QObject *obj);
static void removeObject(QObject *obj);
static QList<QObject *> allObjects();
// 模板方法:按类型取对象
template <typename T>
static T *getObject()
{
QReadLocker lock(listLock());
const QList<QObject *> all = allObjects();
for (QObject *obj : all) {
if (T *result = qobject_cast<T *>(obj))
return result;
}
return nullptr;
}
// 插件操作
static void loadPlugins();
static QList<PluginSpec *> plugins();
void shutdown();
};
注意几个设计细节:
单例不是花哨的单例。 就是最朴素的 static instance(),没有模板,没有 CRTP,没有魔法。因为插件管理器在整个进程里只需要一个,搞复杂了反而难懂。
对象池用了 QReadWriteLock。 读多写少的场景——大部分时间插件在查对象,只在注册/卸载时写。读写锁比互斥锁性能好一个数量级。
getObject 用 qobject_cast。 这比 dynamic_cast 快,前提是你的接口必须继承 QObject。如果某些纯虚接口不想继承 QObject,备选方案是 getInterface() 用 dynamic_cast。
对象池怎么玩?
看一个真实用法——HomePlugin 从对象池里捞主窗口服务:
bool HomePlugin::initialize(QString *errorString)
{
// 从对象池获取主窗口服务(由 CorePlugin 注册)
IMainWindowService *svc = PluginManager::getObject<IMainWindowService>();
if (!svc) {
if (errorString)
*errorString = "HomePlugin: IMainWindowService not found.";
return false;
}
// 注册首页,排在最左边(order=10)
auto *homePage = new SimplePage("page.home.content");
svc->addPage("home", "nav.home", homePage, 10);
return true;
}
插件的通信方式就这么简单: 你往池子里丢对象,我从池子里捞对象。没有头文件依赖,没有复杂的 IPC,就是一个 QList<QObject *>。
这就是对象池模式在工业项目里的真实用法。
三、IPlugin:每个插件的"身份证"
打开 iplugin.h,只有 50 行不到。
class IPlugin : public QObject
{
Q_OBJECT
public:
enum ShutdownFlag {
SynchronousShutdown,
AsynchronousShutdown
};
// 必须实现的两个虚函数
virtual bool initialize(QString *errorString)= 0;
virtual void extensionsInitialized()= 0;
// 可选实现的两个虚函数
virtual bool delayedInitialize(){ return false; }
virtual ShutdownFlag aboutToShutdown(){ return SynchronousShutdown; }
PluginSpec *pluginSpec() const;
signals:
void asynchronousShutdownFinished();
};
C++ Primer 会告诉你: 虚函数是一种多态机制,运行时根据虚表调用派生类实现。
但工业项目会告诉你: IPlugin 的四个虚函数定义了插件的生死节奏。
看这个生命周期:
加载 → initialize → extensionsInitialized → delayedInitialize → ...运行... → aboutToShutdown
每个阶段的用意:
initialize | ||
extensionsInitialized | ||
delayedInitialize | ||
aboutToShutdown |
为什么要分这么多阶段?
因为初始化顺序在插件系统里是个大坑。
如果插件 A 依赖插件 B 的服务,A 在 initialize 里就直接去拿 B 的对象——问题是 B 可能还没加载完。所以把"注册服务"和"使用服务"拆到两个阶段:先跑所有人的 initialize,再跑所有人的 extensionsInitialized。
delayedInitialize 更妙。 它用定时器分批执行,不会阻塞主界面。如果某个插件要加载 500MB 的模型文件,你不会希望它在启动时把界面卡死 10 秒。
四、PluginSpec:插件的"户籍档案"
每个插件都有一个对应的 PluginSpec,相当于身份证加户口本。
class PluginSpec
{
public:
enum State {
Invalid, Read, Resolved,
Loaded, Initialized, Running,
Stopped, Deleted
};
QString name() const;
QString version() const;
QString description() const;
QString category() const;
bool isRequired() const;
QVector<PluginDependency> dependencies() const;
// 依赖解析结果
QHash<PluginDependency, PluginSpec *> dependencySpecs() const;
State state() const;
bool hasError() const;
QString errorString() const;
};
元数据从哪里来?每个插件目录下有个 JSON 文件:
{
"Name": "Home",
"Version": "1.0.0",
"Description" : "Provides the Home page.",
"Category": "UI",
"Required": false,
"Dependencies": [
{ "Name": "Core", "Version": "1.0.0" }
]
}
这个 JSON 就是插件的身份证。 PluginManager 启动时扫描插件目录,反序列化 JSON,构建 PluginSpec 列表。
依赖解析的核心逻辑
问题来了:插件 A 依赖 B,B 依赖 C——加载顺序怎么定?
Aether 的做法是拓扑排序。简单说:
1. 先扫一遍所有插件,构建依赖图
2. 找没有依赖(或依赖已满足)的节点作为起点
3. 加载一个,标记"已就绪",解开依赖它的其他节点
4. 循环,直到全部加载或发现环
代码实现藏在 PluginManagerPrivate::loadQueue() 里:
bool PluginManagerPrivate::loadQueue(PluginSpec *spec,
QList<PluginSpec *> &queue,
QList<PluginSpec *> &circularityCheckQueue)
{
// 如果已经加入队列,说明有环
if (circularityCheckQueue.contains(spec)) {
// 报错:检测到循环依赖
return false;
}
circularityCheckQueue.append(spec);
// 先加载依赖项
for (const auto &dep : spec->dependencies()) {
PluginSpec *depSpec = pluginByName(dep.name);
if (!depSpec) {
// 依赖缺失,报错
continue;
}
loadQueue(depSpec, queue, circularityCheckQueue);
}
if (!queue.contains(spec))
queue.append(spec);
return true;
}
这就是工业级和 toy project 的区别。 你的小项目可以写死 loadPluginA(); loadPluginB();。但 50 个插件互相依赖的生产项目,一个可靠的有环检测的拓扑排序算法是标配。
版本兼容性检查也是一样的道理。PluginSpec::provides() 会比对 CompatVersion,确保加载的插件版本和依赖方期望的版本兼容。不兼容?直接拒绝加载。
五、从零写一个插件
有了上面的基础,现在手把手写一个完整插件。
第一步:写插件类
// myplugin.h
#pragma once
#include "extensionsystem/iplugin.h"
class MyPlugin : public ExtensionSystem::IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID EXTENSIONSYSTEM_IID
FILE "myplugin.json")
public:
MyPlugin() = default;
~MyPlugin() override = default;
bool initialize(QString *errorString) override;
void extensionsInitialized() override{}
};
关键点就两个:
Q_PLUGIN_METADATA宏告诉 Qt "这是一个插件",IID是全局唯一的接口标识符FILE "myplugin.json"指向元数据文件路径
第二步:实现初始化
// myplugin.cpp
#include "myplugin.h"
#include "extensionsystem/pluginmanager.h"
#include "uibase/imainwindowservice.h"
using namespace ExtensionSystem;
bool MyPlugin::initialize(QString *errorString)
{
Q_UNUSED(errorString)
// 注册自己的服务到对象池
auto *myService = new MyService();
PluginManager::addObject(myService);
qDebug() << "[MyPlugin] initialized and service registered.";
return true;
}
记得在 extensionsInitialized 里做跨插件操作。 这里只注册自己的服务,不去碰别人的东西。
第三步:写元数据
// myplugin.json
{
"Name": "MyPlugin",
"Version": "1.0.0",
"CompatVersion": "1.0.0",
"Category": "Business",
"Description" : "My first Aether plugin",
"Required": false,
"Dependencies": [
{ "Name": "Core", "Version": "1.0.0" }
]
}
第四步:配置 CMake
# CMakeLists.txt
set(PLUGIN_NAME MyPlugin)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets)
fz2_collect_sources(PLUGIN_SOURCES PLUGIN_HEADERS)
add_library(${PLUGIN_NAME} SHARED
${PLUGIN_SOURCES}
${PLUGIN_HEADERS}
myplugin.json # ← 元数据也要参与编译!
)
target_compile_features(${PLUGIN_NAME} PRIVATE cxx_std_17)
target_link_libraries(${PLUGIN_NAME} PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Widgets
${COMMON_TARGET_NAMESPACE}::extensionsystem
)
fz2_setup_plugin(${PLUGIN_NAME} "plugins/myplugin")
编译出来就是一个 DLL/SO。 放到程序插件目录,PluginManager 自动扫描加载。
全流程加起来不到 30 行有效代码。你学会了吗?
六、插件系统核心设计模式总结
回顾整篇,Aether 的插件系统用了三个关键模式:
| 单例模式 | ||
| 对象池模式 | ||
| 状态机模式 |
这三板斧组合起来,搞出了一个可以在 50+ 插件规模下稳定运行的生产级框架。
评论区话题: 如果用一句话总结插件系统的核心设计模式,你觉得是什么?单例?策略?还是另有高见?评论区说说你的理解,我每条都会看。
下篇预告: 插件系统让模块各自独立,但模块之间怎么通信?用对象池传递指针太原始了——下一篇,IoC 容器,Laravel 风格的依赖注入在 C++ 中完整落地。我写了 5 年 C++ 才彻底看懂的东西,一篇给你讲透。
系列目录
第 1 篇:[从 60 万行 C++ 项目中我学到了什么?](已发布)
第 2 篇:[Aether 插件架构:为什么我放弃了传统的 MVC](已发布)
第 3 篇:万字拆解 Aether 插件系统,看完你也能写一个(本文)
第 4 篇:我写了 5 年 C++,才彻底看懂 IoC 容器(预告)
夜雨聆风