Введение
Qt поддерживает два принципиально разных режима использования плагинов: динамические (классический случай — отдельные библиотеки, загружаемые в рантайме через QPluginLoader, разобранный в предыдущих статьях) и статические (плагин компилируется и линкуется непосредственно в исполняемый файл приложения, без отдельного файла библиотеки на диске). В статье разберём, в каких сценариях каждый из подходов предпочтительнее, и как технически организовать статическую линковку плагина.
Концепция
Динамические плагины дают реальную возможность добавлять или заменять функциональность после того, как основное приложение уже скомпилировано и установлено — это нужно там, где плагины пишут сторонние разработчики, или где список доступных модулей должен быть изменяемым без пересборки всего продукта. Платой за эту гибкость является сложность управления версионной совместимостью ABI, необходимость распространять отдельные файлы библиотек и риск сбоя при загрузке несовместимого или повреждённого плагина в рантайме.
Статические плагины компилируются в единый исполняемый файл через Q_IMPORT_PLUGIN — это устраняет весь класс проблем динамической загрузки (несовместимость версий, отсутствующие файлы, runtime-сбои загрузки), но требует пересборки всего приложения при изменении набора доступных «плагинов» — то есть фактически это не плагины в полном смысле слова с точки зрения конечного пользователя, а скорее внутренний механизм модульной организации кода самого приложения, использующий тот же программный интерфейс.
Пример кода
// Статический плагин компилируется как обычная статическая библиотека
// CMakeLists.txt для статического плагина
add_library(csvexportplugin STATIC csvexportplugin.cpp)
target_link_libraries(csvexportplugin PRIVATE Qt6::Core)
// csvexportplugin.cpp — реализация идентична динамическому варианту по коду интерфейса
#include "iexportplugin.h"
#include <QObject>
class CsvExportPlugin : public QObject, public IExportPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID IExportPlugin_iid)
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 &filePath, const QByteArray &data) override
{
return true; // реальная логика записи CSV
}
};
// Этот макрос нужен ТОЛЬКО для статических плагинов —
// он экспортирует функцию-фабрику, которую найдёт Q_IMPORT_PLUGIN
#include <QtPlugin>
Q_IMPORT_PLUGIN(CsvExportPlugin)
// main.cpp основного приложения — явная регистрация статических плагинов
#include <QCoreApplication>
#include <QtPlugin>
// Каждый статически линкуемый плагин должен быть явно "импортирован" —
// в отличие от динамических, они не обнаруживаются автоматически при сканировании
Q_IMPORT_PLUGIN(CsvExportPlugin)
Q_IMPORT_PLUGIN(JsonExportPlugin)
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// Статически импортированные плагины уже доступны через
// QPluginLoader::staticInstances() без какой-либо загрузки с диска
for (QObject *pluginInstance : QPluginLoader::staticInstances()) {
if (auto *exporter = qobject_cast<IExportPlugin *>(pluginInstance)) {
qDebug() << "Доступен статический плагин экспорта:" << exporter->formatName();
}
}
return app.exec();
}
# CMakeLists.txt основного приложения
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE Qt6::Core csvexportplugin jsonexportplugin)
Пояснения к коду
В .cpp-файле самого плагина макрос Q_IMPORT_PLUGIN(CsvExportPlugin) присутствует только потому, что показан исходный код самого плагина для наглядности — в реальном проекте этот макрос обычно размещается в main.cpp основного приложения, а не внутри кода плагина (как и показано во втором блоке кода), поскольку именно приложение, статически линкующее плагин, должно явно объявить, какие плагины оно «импортирует» — без этой явной декларации компоновщик может не включить код плагина в финальный бинарник из-за отсутствия каких-либо явных ссылок на него (статическая инициализация через специальный механизм регистрации Qt-плагинов не запускается автоматически без этого макроса).
QPluginLoader::staticInstances() — статический метод, возвращающий список всех объектов статически импортированных плагинов, доступных в текущем процессе — в отличие от динамических плагинов, эти объекты доступны немедленно при старте приложения, без какого-либо явного сканирования директории и вызова load().
Подводные камни
- Забытый
Q_IMPORT_PLUGINв main.cpp — самая частая практическая проблема при работе со статическими плагинами: код плагина компилируется в библиотеку и линкуется без видимых ошибок, но сам плагин не появляется вQPluginLoader::staticInstances(), поскольку компоновщик, не видя явных ссылок на символы плагина, может полностью исключить его код из финального бинарника как «неиспользуемый» (особенно агрессивно при включённой оптимизации линковки с удалением мёртвого кода). - Иллюзия «плагинности» статических плагинов с точки зрения конечного пользователя. Технически статический плагин использует тот же программный интерфейс (
Q_PLUGIN_METADATA, наследование от интерфейса), но конечный пользователь не может добавить новый статический «плагин» без пересборки всего приложения разработчиком — если архитектурная цель действительно в расширяемости третьими сторонами, статические плагины эту цель не решают, несмотря на схожий код. - Размер финального исполняемого файла, растущий с каждым добавленным статическим плагином, поскольку весь их код становится частью единого бинарника — для приложений с большим количеством опциональных модулей это может привести к нежелательно большому размеру дистрибутива, даже если конкретный пользователь использует лишь малую часть из них (в отличие от динамических плагинов, которые можно устанавливать опционально, отдельно от базового приложения).
- Конфликт имён классов между несколькими статическими плагинами, если они не используют достаточно уникальные имена или собственные namespace — поскольку все статические плагины линкуются в единое адресное пространство одного исполняемого файла, в отличие от изолированных адресных пространств отдельных динамических библиотек, столкновение имён символов может приводить к ошибкам линковки, не возникающим при том же наборе плагинов в динамическом варианте.