Qt5で、スタティックプラグインを使ったアプリケーションを作成する方法


はじめに

プラグインに対応したアプリケーションなら、本体側の開発と、拡張機能部分の開発を分離できるのでは、と思いQtを使ったプラグイン対応アプリケーションの作成方法を調べたのでまとめます。
プラグインには、ダイナミックプラグインとスタティックプラグインの2種類があります。
この記事では、スタティックプラグインの作り方をまとめます。

大まかな流れ

プラグインを受け入れる側(本体)の準備
1. プラグイン用のインターフェースクラスを作成する
2. Q_DECLARE_INTERFACEマクロで、インターフェースクラスと識別用のIDを関連づける
3. Q_IMPORT_PLUGINマクロで、インターフェースの実装クラス(プラグイン側)を、スタティックプラグインとしてインポートできるようにする
4. .proファイルにLIBS += <検索ディレクトリ> <リンクするプラグイン名>を追加して、プラグインをリンクする
5. QPluginLoader::staticInstances()を使って、指定の場所からプラグインをロードして使う

プラグイン側の準備
1. プロジェクトを、スタティックプラグインとしてビルドできるように、.proを加筆・修正
2. プラグインのメタデータ用にjsonファイルを作成する。中身はカラでも良い
2. 本体側で定義した、プラグイン用のインターフェースクラスの実装クラスを作成
3. この実装クラスに、Q_PLUGIN_METADATAQ_INTERFACESマクロを記述して、プラグインとしてエクスポートできるようにする

スタティックプラグインを使ったアプリケーションを作成する

アプリケーションの概要

ここでは、先に示した大まか流れに沿って、お絵描きアプリケーションを作っていきます。
お絵描きアプリケーションといっても、ペンツールと消しゴムツールしかない、極シンプルなものです。
この二つのツールを、スタティックプラグインとして作成します。

アプリケーションの構成(概略)

フォルダ構成

プラグインを受け入れる側(本体)の準備(SimplePaint)

プラグイン用のインターフェースクラスを作成する

ITool.h
#include <QtPlugin>

class ITool
{
...
};

#define ITool_iid "jp.tatsuteb.SimplePaint.ITool.v1"
Q_DECLARE_INTERFACE(ITool, ITool_iid)

プラグイン用のインターフェースクラスを作成したら、クラス定義の外側に Q_DECLARE_INTERFACE(Interface ClassName, ID) を記述します。QtPluginのインクルードも忘れずに行ってください。
ロードしたプラグインが、このインターフェースの実装を持っているかどうか照会するのに使われるようです。

インターフェースの実装クラスを、スタティックプラグインとしてインポートできるようにする

main.cpp
#include <QtPlugin>

Q_IMPORT_PLUGIN(PenToolPlugin)
Q_IMPORT_PLUGIN(EraserToolPlugin)

int main(int argc, char *argv[])
{
...
}

Q_IMPORT_PLUGIN(実装クラス名)を記述することで、スタティックプラグインを常に本体側で使えるようにします。

.proファイルを編集して、プラグインをリンクできるようにする

SimplePaint.pro
LIBS    += -L$$PWD/plugins/ -lsp_pentool -lsp_erasertool

if(!debug_and_release|build_pass):CONFIG(debug, debug|release) {
   mac:LIBS = $$member(LIBS, 0) $$member(LIBS, 1)_debug $$member(LIBS, 2)_debug
   win32:LIBS = $$member(LIBS, 0) $$member(LIBS, 1)d $$member(LIBS, 2)d
}

LIBSで、プラグインが格納されているディレクトリと、プラグイン名を指定します。-Lは検索するディレクトリ、-lはライブラリ名を示します。
ここでは、プラグイン名は「sp_pentool(ペンツール)」と「sp_erasertool(消しゴムツール)」にする予定なので、上記のような記述になっています。

ifの中でやっていることは、デバッグビルドとリリースビルドで、プラグインファイル名に_debugや_dがついたりつかなかったりするので、ビルドのタイプに応じてリンクするファイル名を変えています。
$$member(LIBS, 0) は LIBSの0番目の値を取得する、という意味になります。

QPluginLoader::staticInstances() を使って、指定の場所からプラグインをロードして使う

SimplePaint.cpp
#include <QPluginLoader>
#include "ITool.h"

...

QList<ITool *> SimplePaint::loadToolPlugins()
{
    QList<ITool *> tools;
    for (QObject *plugin : QPluginLoader::staticInstances())
    {
        // qobject_castはキャストに失敗すると0を返す
        auto tool = qobject_cast<ITool *>(plugin);

        // ITool以外のプラグインは無視
        if (!tool)
        {
            continue;
        }

        tools << tool;
    }

    return tools;
}

QPluginLoaderは、LIBSで指定したスタティックプラグインを保持しているので、QPluginLoader::staticInstances()を使って、QObjectListとして取得します。QPluginLoaderのインクルードも忘れずに行ってください。

取得したプラグインはqobject_cast<インターフェースクラス *>(plugin)で、適切なクラスにキャストして使用します。キャストに失敗した場合は、ゼロが返ってきます。

プラグインの準備(SimplePaintPenToolPlugin)

プロジェクトを、スタティックプラグインとしてビルドできるように、.proを加筆・修正

SimplePaintPenToolPlugin.pro
QT  += core

TARGET = $$qtLibraryTarget(sp_pentool)
TEMPLATE = lib
CONFIG += plugin static
INCLUDEPATH += $$PWD/../../SimplePaint/
...
DESTDIR = $$PWD/../../SimplePaint/plugins

TARGET に出力するプラグインの名前を指定します。$$qtLibraryTarget(ファイル名)とすることで、デバッグビルドの時にmacでは_debug、Windowsでは_dがファイル名に付加されるようです。

TEMPLATEにはlibを指定します。

今回は、スタティックプラグインとしてビルドしたいので、CONFIGにはpluginとstaticを追加します。

INCLUDEPATHには、本体側に作成したインターフェースクラスが格納されているディレクトリを追加します。これにより、ITool.hがインクルードできるようになります。

DESTDIRには、プラグインを出力するディレクトリへのパスを追加します。

プラグインのメタデータ用にjsonファイルを作成する

PenTool.json
{}

プラグインのメタデータを記述したjsonファイルを作成します。今回は、中身はカラです。

本体側で定義した、プラグイン用インターフェースクラスの実装クラスを作成

PenToolPlugin.h
#include <QObject>
#include <QtPlugin>

#include "ITool.h"

class PenToolPlugin : public QObject, public ITool
{
        Q_OBJECT
        Q_PLUGIN_METADATA(IID "jp.tatsuteb.SimplePaint.ITool.v1" FILE "PenTool.json")
        Q_INTERFACES(ITool)

    public:
        ...
};

Q_OBJECTの後に、Q_PLUGIN_METADATA(IID 本体側に作成したIDと同じもの FILE メタデータ用 jsonファイル)マクロを記述します。このプラグインをエクスポートできるようにします。
QtObjectとQtPluginのインクルードも忘れずに行います。

Q_INTERFACES(インターフェースクラス名)を記述して、基底クラスはプラグインインターフェースであることをmocに伝えます。
このマクロによりqobject_cast()でキャストできるようになるようです。

サンプル

GitHubに、線を描いて消すだけのサンプルを上げておきました。
GitHub - SimplePaint
各ツール(プラグイン)は、ToolPluginProjectsのサブプロジェクトになっています。

参考

主に、公式ドキュメントの「Plug & Paint」を参考にしました。こちらには、ダイナミックプラグインの作り方も詳しく書いてあります。
本体
Qt Documentation - Plug & Paint Example
スタティックプラグイン
Qt Documentation - Plug & Paint Basic Tools Example
ダイナミックプラグイン
Qt Documentation - Plug & Paint Extra Filters Example

おわりに

今回は、スタティックプラグインを使って、簡易お絵描きアプリのツールを実装しました。ところどころ、理解が足りていない部分があるので、今後修正するかもしれません…
スタティックプラグインなので、本体も一緒にビルドする必要があるわけですが、ダイナミックプラグインとして実装すれば、本体をビルドすることなくツールを開発できるので、その方が良かったのかな…と思いました。