Введение
Плагин — это код, скомпилированный отдельно от основного приложения и загружаемый в него во время выполнения, а не на этапе компоновки. Это позволяет расширять функциональность приложения без его пересборки — устанавливая новые модули отдельно, выпуская обновления для конкретного плагина независимо от основного продукта, или позволяя сторонним разработчикам добавлять собственную функциональность. В статье разберём базовый механизм 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(), а не предполагать текущую рабочую директорию.