プラグイン

本アプリは専用のプラグインDLLを読み込み、検索候補となるコマンドを追加できます。 ここでは、C/C++でプラグインを実装するための仕様と実装方法を説明します。

プラグインのAPI定義は、配布物に含まれる plugin-include/soyokaze/PluginExportTable.h が基準です。APIの追加や変更に追従するため、 プラグインのビルドには、使用するSoyokazeと同じバージョンのヘッダを使用してください。

本体のバージョンアップにより、PluginExportTable.hの定義は変更される場合があります。
アプリ本体はプラグインの後方互換性を維持しないため、定義が変更になった場合は追従が必要です。

動作の概要

プラグインは、Soyokazeが入力中のキーワードを検索するときに呼び出されます。 プラグインは入力内容を調べ、検索結果をハンドルとして返します。検索結果1件につき1つのSoyokaze上のコマンドが生成され、ユーザーがそのコマンドを実行するとプラグインの処理が呼び出されます。

処理の流れは次のとおりです。

  1. SoyokazeがプラグインDLLをロードする

  2. LNCRPLUGIN_Bindを呼び出して関数テーブルを取得する

  3. Initializeを呼び出してプラグインを初期化し、プラグイン情報を取得する

  4. キーワード入力のたびにQueryを呼び出す

  5. Queryが返した検索結果を候補として表示する

  6. コマンド実行時にCanExecuteとExecuteを呼び出す

  7. Soyokazeの終了時にFinalizeを呼び出してDLLをアンロードする

DLLはアプリケーションの実行中に一度だけロードされます。プラグインの追加・削除を実行中に反映することはできません。

配置とロード

プラグインは、次のいずれかのpluginsディレクトリへ配置します。

  • Soyokazeの実行ファイルがあるディレクトリ

  • Soyokazeの設定ファイル保存先(ユーザーディレクトリ直下)

実行ファイルがあるディレクトリのpluginsから先にロードされます。プラグインごとにサブディレクトリを作成してください。

実行ファイルのディレクトリ/
  plugins/
    hello/
      hello.dll

ユーザーディレクトリ/
  plugins/
    another-plugin/
      another-plugin.dll

ロード時の規則は次のとおりです。

  • pluginsディレクトリ直下のサブディレクトリを走査します

  • 各サブディレクトリの直下にある.dllをロード対象とします

  • 1つのサブディレクトリに複数のDLLを配置できます

  • サブディレクトリをさらに再帰して探索することはありません

  • pluginsディレクトリが存在しない場合、プラグインはロードされません

  • .dll以外のファイルはロードされません

  • 32bit版はサポートしていません。x64版のSoyokazeに対応するx64 DLLを作成してください

次のいずれかに該当するDLLはロードに失敗します。

  • DLLをロードできない

  • LNCRPLUGIN_Bindを取得できない

  • LNCRPLUGIN_Bindがエラーを返す

  • LNCRPLUGIN_EXPORTTABLEの関数ポインタにnullptrが含まれる

  • Initializeの第2引数から取得したプラグイン情報をJSONとして解析できない

  • プラグイン情報のpluginApiVersionがSoyokazeのPLUGINVERSIONと一致しない

  • Initializeがエラーを返す

ロードに失敗したDLLは保持されず、ロード済みのDLLも終了時まで再ロードされません。

公開するAPI

プラグインはLNCRPLUGIN_Bindという名前の関数をエクスポートします。 関数の宣言はヘッダに定義されています。

int LNCRPLUGIN_API
LNCRPLUGIN_Bind(int version, LNCRPLUGIN_EXPORTTABLE* table);

versionにはSoyokazeが使用するプラグインバージョンが渡されます。プラグイン側では、利用しているPluginExportTable.hのPLUGINVERSIONに対応できるか確認してください。バージョンに対応できない場合は0以外を返します。

正常に関数テーブルを設定できた場合は0を返してください。 tableには、プラグインが実装したLNCRPLUGIN_EXPORTTABLEの各関数ポインタを設定します。現在の実装では、全ての関数ポインタが設定されている必要があります。

LNCRPLUGIN_BindはCリンケージでエクスポートされます。ヘッダをC++からインクルードすれば、宣言に含まれるextern "C"が適用されます。関数名を変更したり、C++の名前修飾が付いた状態でエクスポートしたりしないでください。

関数テーブル

Initialize

int Initialize(LAUNCHER_FUNCTION_TABLE* table, const char** plugin_info);

プラグインのロード直後に一度呼び出されます。設定ファイルの読み込み、インデックスの作成、リソースの確保などをここで行います。

plugin_infoには、プラグインの情報を表すJSON文字列へのポインタを設定してください。JSON文字列の所有権はプラグイン側にあり、Soyokaze本体は文字列を解放しません。Soyokaze本体は受け取ったJSONをコピーして管理します。

LAUNCHER_FUNCTION_TABLEは、Soyokaze本体がプラグインへ渡す関数テーブルです。プラグインからSoyokaze本体の機能を呼び出すための窓口であり、ログ出力、トースト通知、メインウィンドウのハンドル取得に使用できます。構造体の各メンバーには、本体側の機能を呼び出すための関数ポインタが設定されています。

この関数テーブルはプラグインが作成してSoyokazeへ返すものではありません。SoyokazeがInitializeの呼び出し時に作成し、引数としてプラグインへ渡します。プラグインで本体側APIを使用しない場合でも、Initializeの引数を受け取れるようにしてください。

tableが指すLAUNCHER_FUNCTION_TABLEのデータは一時的なものです。後で使用する関数ポインタがある場合は、構造体の内容をプラグイン側でコピーして保持してください。

成功時は0、初期化に失敗した場合は0以外を返します。失敗したプラグインはロードされません。

プラグイン情報

プラグイン情報はJSONオブジェクトで指定します。pluginApiVersionは必須で、プラグインのビルドに使用したPluginExportTable.hのPLUGINVERSIONと一致させてください。pluginIdにはプラグインを識別する一意な文字列を指定してください。その他のキーは表示や管理に使用されます。

{
  "displayName": "プラグイン表示名",
  "pluginId": "プラグインを識別する一意な文字列",
  "pluginVersion": "1.0.0",
  "pluginApiVersion": 102,
  "pluginDescription": "プラグインの概要",
  "pluginDeveloper": "制作者名など",
  "pluginLicenseName": "プラグインのライセンス",
  "url": "プラグインに関するURL"
}

上記以外のキーを追加することもできます。Soyokaze本体が認識しないキーは無視されます。JSONとして解析できない場合や、pluginApiVersionがSoyokaze本体のPLUGINVERSIONと異なる場合、プラグインはロードされません。

Query

LNCRPLUGINMATCHHANDLE Query(void* ctx, MATCHER_FUNCTION_TABLE* table);

入力中のキーワードに対して検索を行い、検索結果を保持するハンドルを返します。検索結果がない場合はnullptrを返してください。

ctxはSoyokazeが管理する検索状態です。MATCHER_FUNCTION_TABLEの関数を呼び出すとき、必ずこの値を第1引数として渡します。ctxの内容をプラグイン側で解釈したり、保持したりしないでください。

tableには、次のキーワード操作関数が設定されています。

関数

内容

Match

指定したキーワードと一致するか調べる

GetFirstWord

入力中の最初のワードを取得する

GetWholeString

入力全体を取得する

GetWordCount

入力中のワード数を取得する

Matchのオフセットは、Pattern::Matchのオフセットと同じ意味です。例えば、最初のワードをプラグインのコマンド名として扱う場合は、2番目以降のワードを検索するためにオフセット1を指定します。

GetMatchCount / GetMatchLevel

int GetMatchCount(LNCRPLUGINMATCHHANDLE handle);
int GetMatchLevel(LNCRPLUGINMATCHHANDLE handle, int index);

GetMatchCountは検索結果の件数を返します。indexは0から始まる検索結果の番号です。

GetMatchLevelは検索結果の一致レベルを返します。値の意味は次のとおりです。

値

意味

5

完全一致

4

前方一致

3

部分一致

2

弱い一致

-1

不一致

GetName / GetDescription / GetGuide / GetTypeDisplayName

int GetName(LNCRPLUGINMATCHHANDLE handle, int index,
            char* buffer, size_t length);
int GetDescription(LNCRPLUGINMATCHHANDLE handle, int index,
            char* buffer, size_t length);
int GetGuide(LNCRPLUGINMATCHHANDLE handle, int index,
            char* buffer, size_t length);
int GetTypeDisplayName(LNCRPLUGINMATCHHANDLE handle, int index,
            char* buffer, size_t length);

これらの関数は、検索結果ごとの文字列を取得します。

関数

画面上での用途

GetName

候補の名前

GetDescription

候補の説明

GetGuide

候補を実行するときのガイド表示

GetTypeDisplayName

コマンド種別の表示

文字列はUTF-8のchar配列として返してください。必要なバッファ長を調べるときはbufferにnullptr、lengthに0を指定します。この場合、終端の\0を含む必要なバイト数を返します。

バッファを渡した場合は、文字列を\0で終端してください。バッファが不足する場合も、可能な範囲で終端文字を含めてコピーします。エラー時は-1を返します。

GetGuideで取得した文字列は、コマンド実行時のアクション表示に使用されます。

CanExecute

int CanExecute(LNCRPLUGINMATCHHANDLE handle, int index);

検索結果を現在実行できるかどうかを返します。0は実行不可、0以外は実行可能です。

実行不可の場合、SoyokazeはGetErrorStringで理由を取得して表示します。

Execute

int Execute(LNCRPLUGINMATCHHANDLE handle, int index,
            int argc, char** argv);

検索結果を実行します。成功時は0、失敗時は0以外を返してください。

argvには実行時引数だけが含まれます。コマンド名は含まれません。

例えば、入力が次のような場合を考えます。

mygrep keyword1 keyword2

プラグインに渡される値は次のとおりです。

argc    = 2
argv[0] = "keyword1"
argv[1] = "keyword2"

引数がない場合はargcが0になります。argvは終端のnullptrを持つダミー配列として渡されます。

GetErrorString

int GetErrorString(LNCRPLUGINMATCHHANDLE handle, int index,
                   char* buffer, size_t length);

CanExecuteまたはExecuteが失敗したときに表示するエラー文字列を返します。文字列の取得方法はGetNameなどの文字列取得関数と同じです。

GetIcon

int GetIcon(LNCRPLUGINMATCHHANDLE handle, int index, HICON* icon);

検索結果に表示するアイコンを返します。成功時は0を返し、iconにアイコンハンドルを設定します。失敗時は0以外を返してください。

返したアイコンの所有権はプラグイン側にあります。アプリ側はアイコンハンドルの解放を行わないため、プラグイン側で適切に解放してください。通常はFinalizeで破棄します。

GetIconに失敗した場合、Soyokazeはデフォルトアイコンを使用します。

CloseHandle

void CloseHandle(LNCRPLUGINMATCHHANDLE handle);

Queryが返した検索ハンドルを破棄します。検索ハンドルに確保したメモリや検索結果の内部データは、この関数で解放してください。

1つの検索ハンドルに複数の検索結果を含めることができます。Soyokazeは検索結果を表示している間、ハンドルを保持します。CloseHandleが呼ばれるまで、ハンドルとその配下の検索結果を有効にしておいてください。

Finalize

void Finalize(void);

Soyokazeの終了時に一度呼び出されます。Initializeで確保したリソース、GetIconで作成したアイコン、設定ファイルやインデックスなどを解放してください。

Finalizeの後にプラグインDLLがアンロードされます。関数から戻った後に、プラグイン内の関数やデータを参照しないでください。

本体側API

LAUNCHER_FUNCTION_TABLEには、Soyokaze本体が提供する次の関数が含まれます。プラグインはInitializeで受け取った構造体を保存し、必要なタイミングで各関数ポインタを呼び出します。

関数

内容

InfoLog

情報ログを出力する

WarnLog

警告ログを出力する

ErrorLog

エラーログを出力する

PopupMessage

トースト通知を表示する

GetMainWindowHandle

メインウィンドウのHWNDを取得する

LoadIconFromPath

ファイルパスに関連付けられたアイコンを取得する

LoadExtensionIcon

ファイル拡張子に関連付けられたアイコンを取得する

HasIcon

アイコンが本体側で管理されているか確認する

OpenFolder

設定されたファイラーでフォルダを開く

アイコン関連の関数の宣言は次のとおりです。

HICON LoadIconFromPath(const char* path);
HICON LoadExtensionIcon(const char* fileExt);
int HasIcon(HICON icon);
int OpenFolder(const char* path);

ログ出力関数とPopupMessageは、UTF-8文字列を受け取ります。 LoadIconFromPathとLoadExtensionIconの引数もUTF-8文字列です。取得したアイコンは本体側が所有するため、プラグイン側で破棄しないでください。 HasIconは、本体側で管理されているアイコンの場合に1、それ以外の場合に0を返します。 OpenFolderの引数はUTF-8文字列です。設定されたファイラーが利用できない場合はExplorerでフォルダを開きます。戻り値は成功時に0、失敗時に0以外です。

static LAUNCHER_FUNCTION_TABLE gLauncher{};
static const char* gPluginInfo = R"({
  "pluginId": "sample-plugin",
  "pluginApiVersion": 102
})";

int Initialize(LAUNCHER_FUNCTION_TABLE* table, const char** plugin_info)
{
    if (table == nullptr || plugin_info == nullptr) {
        return 1;
    }

    // 関数テーブルは呼び出し後も使用するため、コピーして保持する
    gLauncher = *table;
    *plugin_info = gPluginInfo;
    gLauncher.InfoLog("sample plugin initialized");
    gLauncher.PopupMessage("Sample plugin is ready");
    return 0;
}

GetMainWindowHandleで得たHWNDを、Windows APIを呼び出すときの親ウィンドウとして利用できます。

HWND mainWindow = gLauncher.GetMainWindowHandle();
MessageBoxW(mainWindow, L"プラグインからのメッセージ", L"Sample", MB_OK);

最小実装例

以下は、入力がhelloのときに1件の候補を返し、実行時にログを出力する最小構成の例です。実際のDLLでは、検索結果をプラグイン固有の構造体で保持し、Queryで動的に生成してください。

この例では、説明を簡潔にするため、検索ハンドルを1件の検索結果そのものとして扱っています。

#include "PluginExportTable.h"
#include <algorithm>
#include <cstring>
#include <string>

namespace {

LAUNCHER_FUNCTION_TABLE gLauncher{};
const char* gPluginInfo = R"({
  "displayName": "Sample Plugin",
  "pluginId": "sample-plugin",
  "pluginVersion": "1.0.0",
  "pluginApiVersion": 102,
  "pluginDescription": "Sample plugin command"
})";

struct Match {
    std::string name = "hello";
    std::string description = "Sample plugin command";
    std::string guide = "Run the sample plugin";
    std::string type = "Sample";
};

int CopyString(const std::string& value, char* buffer, size_t length)
{
    const size_t required = value.size() + 1;
    if (buffer == nullptr || length == 0) {
        return static_cast<int>(required);
    }

    const size_t copied = std::min(value.size(), length - 1);
    std::memcpy(buffer, value.data(), copied);
    buffer[copied] = '\0';
    return static_cast<int>(copied);
}

int Initialize(LAUNCHER_FUNCTION_TABLE* table, const char** plugin_info)
{
    if (table == nullptr || plugin_info == nullptr) {
        return 1;
    }
    gLauncher = *table;
    *plugin_info = gPluginInfo;
    return 0;
}

LNCRPLUGINMATCHHANDLE Query(void* ctx, MATCHER_FUNCTION_TABLE* table)
{
    if (ctx == nullptr || table == nullptr || table->GetWholeString == nullptr) {
        return nullptr;
    }

    const char* input = table->GetWholeString(ctx);
    if (input == nullptr || std::strcmp(input, "hello") != 0) {
        return nullptr;
    }
    return new Match();
}

int GetMatchCount(LNCRPLUGINMATCHHANDLE)
{
    return 1;
}

int GetMatchLevel(LNCRPLUGINMATCHHANDLE, int)
{
    return 5;
}

int GetName(LNCRPLUGINMATCHHANDLE handle, int, char* buffer, size_t length)
{
    return CopyString(static_cast<Match*>(handle)->name, buffer, length);
}

int GetDescription(LNCRPLUGINMATCHHANDLE handle, int, char* buffer, size_t length)
{
    return CopyString(static_cast<Match*>(handle)->description, buffer, length);
}

int GetGuide(LNCRPLUGINMATCHHANDLE handle, int, char* buffer, size_t length)
{
    return CopyString(static_cast<Match*>(handle)->guide, buffer, length);
}

int GetTypeDisplayName(LNCRPLUGINMATCHHANDLE handle, int, char* buffer, size_t length)
{
    return CopyString(static_cast<Match*>(handle)->type, buffer, length);
}

int CanExecute(LNCRPLUGINMATCHHANDLE, int)
{
    return 1;
}

int Execute(LNCRPLUGINMATCHHANDLE, int, int argc, char** argv)
{
    if (gLauncher.InfoLog != nullptr) {
        gLauncher.InfoLog(argc == 0 ? "hello executed" : argv[0]);
    }
    return 0;
}

int GetErrorString(LNCRPLUGINMATCHHANDLE, int, char*, size_t)
{
    return 0;
}

int GetIcon(LNCRPLUGINMATCHHANDLE, int, HICON*)
{
    return 1;
}

void CloseHandle(LNCRPLUGINMATCHHANDLE handle)
{
    delete static_cast<Match*>(handle);
}

void Finalize()
{
}

} // 名前空間

extern "C" int LNCRPLUGIN_API
LNCRPLUGIN_Bind(int version, LNCRPLUGIN_EXPORTTABLE* table)
{
    if (table == nullptr || version != PLUGINVERSION) {
        return 1;
    }

    *table = {
        &Initialize,
        &Finalize,
        &Query,
        &GetMatchCount,
        &GetMatchLevel,
        &CloseHandle,
        &GetName,
        &GetDescription,
        &GetGuide,
        &GetTypeDisplayName,
        &CanExecute,
        &Execute,
        &GetErrorString,
        &GetIcon,
    };
    return 0;
}

この例をDLLとしてビルドするには、Soyokazeから取得したPluginExportTable.hをインクルードし、x64のDLLプロジェクトとしてビルドします。生成したDLLをplugins配下のプラグイン用サブディレクトリに配置し、Soyokazeを再起動してください。

文字コードとABI

PluginExportTable.hで定義されたchar*型の文字列はUTF-8です。Windowsのワイド文字列を扱う場合は、プラグイン側でUTF-8との変換を行ってください。

プラグインとSoyokaze本体は、同じPluginExportTable.hの定義、同じCPUアーキテクチャ、互換性のあるビルド環境で使用してください。32bit DLLをx64版Soyokazeからロードすることはできません。

LNCRPLUGINMATCHHANDLEの実体はプラグイン側が定義します。ハンドルはプラグイン間で共通に見えても、異なるプラグインのハンドルを別のプラグインの関数に渡すことはできません。

実装時の注意

  • Initializeに渡されたLAUNCHER_FUNCTION_TABLEは、必要に応じてコピーして保持する

  • Queryが返したハンドルは、CloseHandleが呼ばれるまで有効にする

  • GetMatchCountが返す件数と、各APIに渡されるindexの範囲を一致させる

  • 文字列はUTF-8で返し、必要なバッファ長には終端文字を含める

  • GetIconで返したアイコンはプラグイン側で適切に解放する

  • LoadIconFromPathとLoadExtensionIconで返したアイコンは本体側で管理されるため、プラグイン側で破棄しない

  • HasIconの戻り値は、管理対象の場合が1、管理対象外の場合が0

  • Executeの戻り値は、成功時だけ0にする

  • Finalizeから戻った後にプラグインのリソースを参照しない

  • APIのバージョンが合わない場合はLNCRPLUGIN_Bindでエラーを返す

プラグインはSoyokazeのプロセス内で動作します。プラグイン内で発生した未処理例外やアクセス違反は、本体の動作にも影響します。外部ファイルの読み込みやWindows APIの呼び出しなど、プラグイン固有の処理については適切にエラー処理を行ってください。