Система плагинов

Метаданные плагинов через JSON (Q_PLUGIN_METADATA) и поиск по ним

2 просмотров

Введение

Помимо самого IID интерфейса, плагин может нести произвольные дополнительные метаданные — версию, автора, список поддерживаемых форматов файлов, требования к окружению — через JSON-блок, встроенный непосредственно в Q_PLUGIN_METADATA. Эти метаданные доступны через QPluginLoader::metaData() ещё до фактической загрузки и инстанцирования плагина — это позволяет приложению предварительно отфильтровать доступные плагины (например, по совместимости или категории) без накладных расходов на загрузку каждого из них в память. В статье разберём практическое использование метаданных для системы плагинов с категоризацией.

Концепция

JSON-метаданные указываются через дополнительный параметр FILE макроса Q_PLUGIN_METADATA, ссылающийся на отдельный .json-файл, содержимое которого встраивается в скомпилированную библиотеку как часть метаданных плагина. QPluginLoader::metaData() возвращает QJsonObject, где пользовательские данные из этого файла доступны внутри вложенного объекта "MetaData" — это разделение позволяет Qt хранить и собственные системные поля (IID, версия Qt, архитектура) на одном уровне, а произвольные пользовательские данные — на другом, не конфликтующем уровне.

Пример кода

// csvexportplugin.json — метаданные плагина, встраиваемые при сборке
{
    "version": "1.2.0",
    "author": "ООО МойКомпани",
    "category": "export",
    "supportedExtensions": ["csv", "tsv"],
    "minimumAppVersion": "3.0.0"
}
// csvexportplugin.cpp — подключение JSON-метаданных к плагину
#include "iexportplugin.h"

class CsvExportPlugin : public QObject, public IExportPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID IExportPlugin_iid FILE "csvexportplugin.json")
    Q_INTERFACES(IExportPlugin)

public:
    QString formatName() const override { return "CSV таблица"; }
    QString fileExtension() const override { return "csv"; }
    bool canExport(const QVariantMap &) const override { return true; }
    bool exportToFile(const QString &, const QByteArray &) override { return true; }
};
// pluginscanner.h — сканирование директории плагинов с предварительной фильтрацией
#pragma once
#include <QDir>
#include <QPluginLoader>
#include <QJsonObject>
#include <QVersionNumber>
#include <QDebug>

class PluginScanner
{
public:
    struct PluginInfo
    {
        QString filePath;
        QString category;
        QVersionNumber version;
        QStringList supportedExtensions;
    };

    QList<PluginInfo> scanDirectory(const QString &pluginsDir, const QString &requiredCategory)
    {
        QList<PluginInfo> result;
        QDir dir(pluginsDir);

        for (const QString &fileName : dir.entryList(QDir::Files)) {
            const QString filePath = dir.absoluteFilePath(fileName);
            QPluginLoader loader(filePath);

            // Метаданные читаются БЕЗ фактической загрузки кода плагина в память —
            // это быстрая, "дешёвая" операция по сравнению с loader.instance()
            const QJsonObject metaData = loader.metaData();
            if (metaData.isEmpty()) {
                continue; // не Qt-плагин вовсе
            }

            const QJsonObject userData = metaData.value("MetaData").toObject();
            const QString category = userData.value("category").toString();

            if (category != requiredCategory) {
                continue; // фильтрация по категории без загрузки кода
            }

            PluginInfo info;
            info.filePath = filePath;
            info.category = category;
            info.version = QVersionNumber::fromString(userData.value("version").toString());
            for (const auto &ext : userData.value("supportedExtensions").toArray()) {
                info.supportedExtensions.append(ext.toString());
            }
            result.append(info);
        }

        return result;
    }
};

Пояснения к коду

JSON-файл csvexportplugin.json лежит рядом с исходным кодом плагина и подключается через FILE "csvexportplugin.json" внутри Q_PLUGIN_METADATA — на этапе компиляции содержимое файла встраивается в бинарный плагин, и далее доступно через рефлексию метаданных без необходимости в исходном .json-файле на диске после сборки (он нужен только во время компиляции).

PluginScanner::scanDirectory() демонстрирует ключевое практическое преимущество JSON-метаданных: фильтрация по category происходит на основе loader.metaData(), вызов которого не требует фактической загрузки кода библиотеки в память процесса (в отличие от loader.instance(), который выполняет полную загрузку и инициализацию) — это позволяет эффективно просканировать директорию с большим числом плагинов разных категорий и инициализировать только те, что реально нужны в текущем контексте, не тратя время и память на загрузку нерелевантных.

Подводные камни

  • Различие между системными полями метаданных Qt и пользовательскими данными внутри "MetaData". Начинающие разработчики иногда пытаются прочитать своё пользовательское поле напрямую из корня QJsonObject, возвращённого metaData(), без обращения к вложенному объекту "MetaData" — и получают пустое значение, поскольку Qt явно разделяет собственные системные поля (IID, className, version самого Qt) от пользовательских данных файла, указанного через FILE.
  • Несинхронизация версии в JSON-метаданных ("version": "1.2.0") с реальной версией кода плагина. Поскольку это просто текстовое поле в отдельном файле, ничто не гарантирует автоматическую синхронизацию с фактической версией собранного бинарника — если процесс релиза плагинов не автоматизирован (например, через генерацию JSON-файла на основе единого источника версии в системе сборки), эти два значения легко рассинхронизировать вручную.
  • Отсутствие проверки minimumAppVersion в показанном коде сканера. Метаданные содержат поле, заявляющее минимальную версию приложения, необходимую для совместимости плагина, но PluginScanner в примере его не проверяет — для полноценной защиты от загрузки плагина, разработанного под более новую версию API приложения, чем фактически установлена, нужна явная проверка этого поля перед попыткой инстанцирования плагина через loader.instance().
  • JSON-файл метаданных, ссылающийся на несуществующий путь или содержащий синтаксически некорректный JSON, приводит к ошибке на этапе компиляции плагина (поскольку Qt разбирает и встраивает этот файл при сборке) — но если ошибка в самом JSON-синтаксисе незначительна и компилятор её каким-то образом пропускает, итоговые метаданные плагина в рантайме могут оказаться частично пустыми, что обнаружится только при попытке прочитать конкретное ожидаемое поле через metaData().