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

QPluginLoader: основы динамической загрузки плагинов

1 просмотров

Введение

Плагин — это код, скомпилированный отдельно от основного приложения и загружаемый в него во время выполнения, а не на этапе компоновки. Это позволяет расширять функциональность приложения без его пересборки — устанавливая новые модули отдельно, выпуская обновления для конкретного плагина независимо от основного продукта, или позволяя сторонним разработчикам добавлять собственную функциональность. В статье разберём базовый механизм QPluginLoader для загрузки динамической библиотеки как Qt-плагина.

Концепция

Плагин Qt — это динамическая библиотека (.dll/.so/.dylib), экспортирующая один объект через специальный механизм Q_PLUGIN_METADATA и реализующая один или несколько интерфейсов, известных загружающему приложению. QPluginLoader загружает библиотеку по указанному пути, проверяет совместимость метаданных (версия Qt, используемая для сборки плагина, должна быть совместима с версией приложения) и предоставляет указатель на корневой объект плагина через instance(), который затем приводится (qobject_cast) к ожидаемому интерфейсу.

Пример кода

// iimagefilterplugin.h — интерфейс, который должны реализовывать плагины
#pragma once
#include <QObject>
#include <QImage>
#include <QString>

class IImageFilterPlugin
{
public:
    virtual ~IImageFilterPlugin() = default;
    virtual QString filterName() const = 0;
    virtual QImage applyFilter(const QImage &input) const = 0;
};

#define IImageFilterPlugin_iid "com.mycompany.myapp.IImageFilterPlugin/1.0"
Q_DECLARE_INTERFACE(IImageFilterPlugin, IImageFilterPlugin_iid)
// grayscalefilterplugin.h — конкретная реализация плагина
#pragma once
#include "iimagefilterplugin.h"
#include <QObject>

class GrayscaleFilterPlugin : public QObject, public IImageFilterPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID IImageFilterPlugin_iid)
    Q_INTERFACES(IImageFilterPlugin)

public:
    QString filterName() const override { return "Чёрно-белый фильтр"; }

    QImage applyFilter(const QImage &input) const override
    {
        return input.convertToFormat(QImage::Format_Grayscale8);
    }
};
// Загрузка и использование плагина в основном приложении
#include <QPluginLoader>
#include <QDir>
#include <QDebug>
#include "iimagefilterplugin.h"

void loadAndUsePlugin(const QString &pluginPath)
{
    QPluginLoader loader(pluginPath);

    QObject *pluginInstance = loader.instance();
    if (!pluginInstance) {
        qWarning() << "Не удалось загрузить плагин:" << loader.errorString();
        return;
    }

    auto *filterPlugin = qobject_cast<IImageFilterPlugin *>(pluginInstance);
    if (!filterPlugin) {
        qWarning() << "Плагин не реализует ожидаемый интерфейс IImageFilterPlugin";
        loader.unload();
        return;
    }

    qDebug() << "Загружен плагин:" << filterPlugin->filterName();

    QImage testImage(100, 100, QImage::Format_RGB32);
    testImage.fill(Qt::red);
    const QImage result = filterPlugin->applyFilter(testImage);
    qDebug() << "Фильтр применён, формат результата:" << result.format();
}
# CMakeLists.txt для самого плагина — собирается как разделяемая библиотека
add_library(grayscalefilterplugin SHARED grayscalefilterplugin.cpp)
target_link_libraries(grayscalefilterplugin PRIVATE Qt6::Core Qt6::Gui)
set_target_properties(grayscalefilterplugin PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins"
)

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

IImageFilterPlugin — чистый абстрактный интерфейс (по аналогии со статьёй про абстрактные интерфейсы из основного цикла), а макрос Q_DECLARE_INTERFACE с уникальным строковым идентификатором (IID) регистрирует его в метаобъектной системе Qt, что необходимо для последующего безопасного приведения типа через qobject_cast между разными модулями (основным приложением и динамически загруженной библиотекой плагина).

GrayscaleFilterPlugin наследует одновременно QObject и интерфейс IImageFilterPlugin — комбинация, разрешённая правилами множественного наследования Qt, разобранными в статье 153 (один QObject плюс произвольное число не-QObject интерфейсов). Макрос Q_PLUGIN_METADATA(IID IImageFilterPlugin_iid) помечает класс как плагин, экспортируемый именно под этим идентификатором интерфейса, а Q_INTERFACES явно регистрирует поддержку интерфейса в метаобъектной системе для корректной работы qobject_cast.

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

  • Несовпадение версии Qt, под которой собран плагин, и версии Qt, под которой собрано основное приложение. QPluginLoader явно проверяет совместимость по бинарной версии Qt — попытка загрузить плагин, собранный под другую минорную версию (или с другими настройками сборки — Debug/Release, статическая/динамическая линковка с CRT на Windows), завершится ошибкой loader.errorString(), явно описывающей причину несовместимости.
  • Отсутствие проверки результата qobject_cast перед использованием. Если загруженная библиотека технически является валидным Qt-плагином, но не реализует ожидаемый интерфейс (например, по ошибке, или это плагин совершенно другого типа), qobject_cast вернёт nullptr — использование результата без проверки приведёт к разыменованию нулевого указателя.
  • Утечка памяти или ресурсов при многократной загрузке/выгрузке плагинов без вызова unload(). Каждый QPluginLoader, успешно загрузивший библиотеку, должен явно вызвать unload(), когда плагин больше не нужен — простое удаление объекта QPluginLoader без явного unload() не гарантированно освобождает саму загруженную динамическую библиотеку из памяти процесса во всех случаях.
  • Путь к плагину, зависящий от текущей рабочей директории процесса, а не от пути установки приложения — относительные пути к директории плагинов могут работать при запуске из IDE (где рабочая директория предсказуема), но ломаться при запуске собранного приложения из произвольного места — путь к директории плагинов надёжнее формировать через QCoreApplication::applicationDirPath(), а не предполагать текущую рабочую директорию.