11#include <QtCore/QDirIterator>
12#include <QtCore/QFileInfo>
13#include <QtCore/QJsonObject>
14#include <QtCore/QPluginLoader>
15#include <QtCore/QQueue>
31bool isPluginFileName(
const QString &
name)
33 return name.endsWith(QLatin1String(
".so")) ||
name.endsWith(QLatin1String(
".dll")) ||
34 name.endsWith(QLatin1String(
".dylib")) ||
name.endsWith(QLatin1String(
".bundle"));
37const QString kIidKey = QStringLiteral(
"IID");
38const QString kMetaDataKey = QStringLiteral(
"MetaData");
42class QxPluginManagerPrivate
51 bool isStatic =
false;
53 QStringList pluginPaths;
54 QSet<QString> disabledPlugins;
55 QSet<QString> enabledPlugins;
56 int requiredInterfaceVersion = 1;
57 QVersionNumber hostVersion;
58 bool hostVersionSet =
false;
62 QList<QxPluginSpec *> orderedSpecs;
63 QHash<QString, QxPluginSpec *> specById;
75 QList<QxPluginSpec *> staticSpecs;
76 QList<QxPluginSpec *> dynamicSpecs;
77 QList<QPluginLoader *> dynamicLoaders;
78 QList<QxPluginManagerPrivate::Entry *> staticEntries;
79 QList<QxPluginManagerPrivate::Entry *> dynamicEntries;
80 QList<QxPluginManagerPrivate::Entry *> entries;
83 bool hasError =
false;
87 QString cycleChain(QxPluginSpec *start,
const QList<QxPluginSpec *> &live)
const;
88 QxPlugin *createInstance(QxPluginSpec *spec);
91void QxPluginManagerPrivate::gatherDynamic()
93 qDeleteAll(dynamicSpecs);
95 qDeleteAll(dynamicLoaders);
96 dynamicLoaders.clear();
97 qDeleteAll(dynamicEntries);
98 dynamicEntries.clear();
100 entries.append(staticEntries);
102 for (
const QString &path : std::as_const(pluginPaths)) {
106 const QFileInfoList files = dir.entryInfoList(QDir::Files);
107 for (
const QFileInfo &fi : files) {
108 if (!isPluginFileName(fi.fileName()))
110 auto *loader =
new QPluginLoader(fi.absoluteFilePath());
111 dynamicLoaders.append(loader);
112 const QJsonObject md = loader->metaData();
115 auto *spec =
new QxPluginSpec;
116 dynamicSpecs.append(spec);
117 if (!spec->read(fi.absoluteFilePath(), md.value(kMetaDataKey).toObject()))
118 spec->setState(QxPluginState::Disabled);
119 auto *entry =
new QxPluginManagerPrivate::Entry{spec, loader, {},
false};
120 dynamicEntries.append(entry);
121 entries.append(entry);
126QString QxPluginManagerPrivate::cycleChain(QxPluginSpec *start,
const QList<QxPluginSpec *> &live)
const
130 QxPluginSpec *cur = start;
131 while (cur && !seen.contains(cur->id())) {
132 seen.insert(cur->id());
133 chain.append(cur->id());
138 QStringList edges = cur->dependencies();
139 edges += cur->optionalDependencies();
140 for (
const QString &dep : std::as_const(edges)) {
141 QxPluginSpec *depSpec = specById.value(dep);
148 if (live.contains(depSpec)) {
156 chain.append(cur->id());
157 return chain.join(QStringLiteral(
" -> "));
160void QxPluginManagerPrivate::resolve()
162 Q_Q(QxPluginManager);
164 orderedSpecs.clear();
168 for (QxPluginManagerPrivate::Entry *e : std::as_const(entries)) {
169 QxPluginSpec *s = e->spec;
170 const QString
id = s->id();
171 if (specById.contains(
id)) {
172 s->setState(QxPluginState::Disabled);
173 s->setError(QStringLiteral(
"duplicate plugin id '%1'").arg(
id));
176 specById.insert(
id, s);
180 for (QxPluginManagerPrivate::Entry *e : std::as_const(entries)) {
181 QxPluginSpec *s = e->spec;
182 if (s->state() == QxPluginState::Disabled)
184 if (disabledPlugins.contains(s->id())) {
185 s->setState(QxPluginState::Disabled);
191 if (!s->isEnabledByDefault() && !enabledPlugins.contains(s->id())) {
192 s->setState(QxPluginState::Disabled);
195 if (hostVersionSet) {
196 const QVersionNumber lo = s->compatVersion();
197 const QVersionNumber hi = s->version();
198 if (hostVersion < lo || hostVersion > hi) {
199 s->setState(QxPluginState::Disabled);
200 s->setError(QStringLiteral(
"plugin '%1' requires host version in [%2, %3], but the host is %4")
201 .arg(s->id(), lo.toString(), hi.toString(), hostVersion.toString()));
205 bool missing =
false;
206 for (
const QString &dep : s->dependencies()) {
207 if (!specById.contains(dep)) {
208 s->setState(QxPluginState::Disabled);
209 s->setError(QStringLiteral(
"plugin '%1' is missing required dependency '%2'").arg(s->id(), dep));
223 for (QxPluginManagerPrivate::Entry *e : std::as_const(entries)) {
224 QxPluginSpec *s = e->spec;
225 if (s->state() != QxPluginState::Read)
227 for (
const QString &dep : s->dependencies()) {
228 QxPluginSpec *depSpec = specById.value(dep);
229 if (depSpec && depSpec->state() == QxPluginState::Disabled) {
230 s->setState(QxPluginState::Disabled);
231 if (s->error().isEmpty())
233 QStringLiteral(
"plugin '%1' depends on '%2', which is not available").arg(s->id(), dep));
242 QList<QxPluginSpec *> live;
243 for (QxPluginManagerPrivate::Entry *e : std::as_const(entries)) {
244 QxPluginSpec *s = e->spec;
245 if (s->state() == QxPluginState::Read)
248 const QSet<QxPluginSpec *> liveSet(live.begin(), live.end());
250 QHash<QString, QList<QxPluginSpec *>> dependents;
251 QHash<QString, int> inDegree;
252 for (QxPluginSpec *s : live)
253 inDegree.insert(s->
id(), 0);
254 for (QxPluginSpec *s : live) {
255 for (
const QString &dep : s->dependencies()) {
256 dependents[dep].append(s);
265 for (
const QString &dep : s->optionalDependencies()) {
266 if (!liveSet.contains(specById.value(dep)))
268 dependents[dep].append(s);
273 QQueue<QxPluginSpec *> queue;
274 for (QxPluginSpec *s : live)
275 if (inDegree.value(s->
id()) == 0)
278 QList<QxPluginSpec *> order;
279 while (!queue.isEmpty()) {
280 QxPluginSpec *s = queue.dequeue();
282 s->setState(QxPluginState::Resolved);
283 for (QxPluginSpec *dep : dependents.value(s->
id())) {
284 inDegree[dep->id()]--;
285 if (inDegree.value(dep->id()) == 0)
291 if (order.size() < live.size()) {
292 for (QxPluginSpec *s : live) {
293 if (order.contains(s))
295 s->setState(QxPluginState::Disabled);
297 QStringLiteral(
"plugin '%1' is part of a circular dependency: %2").arg(s->id(), cycleChain(s, live)));
301 orderedSpecs = order;
304QxPlugin *QxPluginManagerPrivate::createInstance(QxPluginSpec *spec)
306 for (QxPluginManagerPrivate::Entry *e : entries) {
317 instance->setParent(&pluginOwner);
321 return qobject_cast<QxPlugin *>(e->loader->instance());
329QxPluginManager::QxPluginManager(QObject *parent)
336 qDeleteAll(d->staticEntries);
337 qDeleteAll(d->staticSpecs);
338 qDeleteAll(d->dynamicEntries);
339 qDeleteAll(d->dynamicSpecs);
340 qDeleteAll(d->dynamicLoaders);
347 d->pluginPaths = paths;
353 return d->pluginPaths;
359 d->disabledPlugins = QSet<QString>(ids.begin(), ids.end());
365 return QStringList(d->disabledPlugins.begin(), d->disabledPlugins.end());
371 d->enabledPlugins = QSet<QString>(ids.begin(), ids.end());
377 return QStringList(d->enabledPlugins.begin(), d->enabledPlugins.end());
383 d->requiredInterfaceVersion = version;
389 return d->requiredInterfaceVersion;
395 d->hostVersion = version;
396 d->hostVersionSet =
true;
402 return d->hostVersion;
408 return d->hostVersionSet;
424 std::function<
QxPlugin *()> factory)
428 d->staticSpecs.append(
spec);
431 d->staticEntries.append(
new QxPluginManagerPrivate::Entry{
spec,
Q_NULLPTR, factory,
true});
440 d->errorString.clear();
443 for (
QxPluginSpec *s : std::as_const(d->orderedSpecs)) {
445 bool depFailed =
false;
446 for (
const QString &dep : s->dependencies()) {
450 if (s->error().isEmpty())
452 QStringLiteral(
"plugin '%1' depends on '%2', which is not available").arg(s->id(), dep));
464 QxPlugin *instance = d->createInstance(s);
467 s->setError(QStringLiteral(
"plugin '%1' could not be instantiated").arg(s->id()));
471 s->setPlugin(instance);
476 s->setError(QStringLiteral(
"plugin '%1' interface version %2 is below the required %3")
479 .arg(d->requiredInterfaceVersion));
486 if (!instance->
initialize(d->context, &err)) {
488 s->setError(err.isEmpty() ? QStringLiteral(
"plugin '%1' returned false from initialize()").arg(s->id())
502 for (
QxPluginSpec *s : std::as_const(d->orderedSpecs)) {
504 s->plugin()->extensionsInitialized();
509 for (QxPluginManagerPrivate::Entry *e : std::as_const(d->entries)) {
511 if (!s->
error().isEmpty()) {
512 lines.append(QStringLiteral(
"%1: %2").arg(s->
id(), s->
error()));
516 d->errorString = lines.join(QLatin1String(
"\n"));
523 QList<QxPluginSpec *> reverse = d->orderedSpecs;
524 std::reverse(reverse.begin(), reverse.end());
527 s->plugin()->shutdown();
536 return d->orderedSpecs;
542 QList<QxPluginSpec *>
specs;
543 specs.reserve(d->staticEntries.count() + d->dynamicEntries.count());
544 for (QxPluginManagerPrivate::Entry *e : d->staticEntries)
545 specs.append(e->spec);
546 for (QxPluginManagerPrivate::Entry *e : d->dynamicEntries)
547 specs.append(e->spec);
554 return d->specById.value(
id);
573 return d->errorString;
The thin surface a plugin programs against.
Discovers, orders, loads and isolates QtCanpool plugins.
void loadPlugins()
Scans the paths (or the registered static plugins), resolves dependencies and loads in order.
~QxPluginManager() override
QStringList enabledPlugins() const
QList< QxPluginSpec * > specs() const
All specs in dependency order (the order loadPlugins() would load them).
void registerStaticPlugin(const QString &key, const QJsonObject &metaData, std::function< QxPlugin *()> factory)
Static-build entry (WASM / QTC_STATIC_BUILD): register a pre-built instance factory.
void setContext(QxPluginContext *context)
The context handed to every plugin's initialize(); set before loadPlugins().
void setEnabledPlugins(const QStringList &ids)
Ids to start even though their metadata says EnabledByDefault is false.
void shutdown()
Calls shutdown() on every initialized plugin, in reverse initialization order.
QxPlugin * plugin(const QString &id) const
void setHostVersion(const QVersionNumber &version)
The host version plugins must be compatible with; accept-all until set.
void setPluginPaths(const QStringList &paths)
void setRequiredInterfaceVersion(int version)
Minimum interface version a plugin must report to be accepted (default 1).
QVersionNumber hostVersion() const
int requiredInterfaceVersion() const
bool hasError() const
True when at least one plugin failed (load or initialize).
QList< QxPluginSpec * > allSpecs() const
Every plugin the manager knows about, in discovery order (the static ones first, then the dynamic one...
QStringList pluginPaths() const
bool hasHostVersion() const
QStringList disabledPlugins() const
QxPluginSpec * spec(const QString &id) const
void pluginFailed(QxPluginSpec *spec, const QString &reason)
A plugin was set aside; reason is the same string spec()->error() holds.
void setDisabledPlugins(const QStringList &ids)
Ids that never start, whatever their metadata says.
void pluginInitialized(QxPluginSpec *spec)
A plugin reached the Initialized state.
QString errorString() const
Every failure joined once, each prefixed by its plugin id.
QxPluginContext * context() const
A read-only view of one plugin's metadata, plus the runtime state the manager attaches to it.
QString error() const
Why the plugin is not running; empty when it runs or is only skipped by default.
bool read(const QString &filePath, const QJsonObject &metaData)
Fills the spec from metadata; returns false (and records error()) on a malformed document.
QxPluginState state() const
QxPlugin * plugin() const
The live instance, or null until Loaded.
QString id() const
Stable plugin id; also the base name of its library and metadata file.
QxPluginContext * m_context
Set by the manager before initialize(); also reachable through context().
virtual int interfaceVersion() const
The plugin interface version this build targets.
virtual bool initialize(QxPluginContext *context, QString *errorString)=0
Called after every required dependency has been initialized.
Copyright (C) 2026 maminjie canpool@163.com SPDX-License-Identifier: MulanPSL-2.0
#define QX_INIT_PRIVATE(Class)
#define QX_DECLARE_PUBLIC(Class)
#define Q_NULLPTR
Copyleft (C) 2025 maminjie canpool@163.com SPDX-License-Identifier: MulanPSL-2.0
#define QX_PLUGIN_BEGIN_NAMESPACE
#define QX_PLUGIN_END_NAMESPACE