プラグイン対応アプリケーションについて

reshia
Last update: 2006-06-30

Table of Contents

1. はじめに

アプリケーション開発をしているとよく後から機能を追加したいと思うことがあります。このとき普通はソースファイルに手を加えて機能を拡張します。しかし、ソースファイルに手を加えようとするとアプリケーション自体の仕様を変更することになったり、誤った修正を行ってしまうかもしれません。また、ある機能を差し替えを行う場合にも同様の問題が起こり得ます。たとえば、アプリケーション開発の最初から機能の修正や追加、変更が行われることがわかっている(予測できる)場合、「プラグイン」というものを使って、この種の問題を解決できます。

本ドキュメントでは、機能拡張や機能変更などを容易に行えるアプリケーション開発について説明します。またプラグインに関する様々な問題や注意点もまとめておきます。

2. 特徴

2.1. プラグインの特徴

プラグインとは、アプリケーションの機能を拡張するプログラムのことです。アプリケーション本体とは独立していますので、プラグインの機能を修正したい場合にもアプリケーション本体に手を加えることなく修正が可能です(図1)。「アドオン」や「エクステンション」などとも呼ばれます。

 |----------------------|  |-----------|
 |            |++|プラグイン1|
 |            |  |-----------|
 |            |
 |            |  |-----------|
 | アプリケーション本体 |++|プラグイン2|
 |            |  |-----------|
 |            |
 |            |  |-----------|
 |            |++|プラグイン3|
 |----------------------|  |-----------|
   ( 図1:プラグインの概念図 )

2.2. プラグインを使うメリット

(1) いちいちビルドし直す必要がない。

アプリケーションとプラグインは、別々のプログラムですので、いちいちアプリケーション全体をビルドし直す必要はありません。つまり、アプリケーション自体は、1度作ってしまえば使い回しができます。

(2)工数が少なくなる。

プラグイン化することで、モジュール独立性が高まります。一般的にモジュール独立性が高まると、アプリケーション全体(拡張機能も含む)の開発にかかる工数が減る傾向にあることが知られています。

(3)機能付加、削除、修正が容易になる。

モジュール独立性が高いため、機能の追加、修正、削除が簡単に行えます。さらにいえばアプリケーション開発者とプラグイン開発者が同じ人物である必要性がないため、プラグインを含めたアプリケーション全体としての開発を並行的に行えるようになります。

(4)実行時に機能を切り替えられる

プラグインは実行時にも追加と削除が可能です。このためアプリケーション使用者は、自分の目的に合ったプラグインを使うことが可能になります。

たとえば、CPU毎に最適化したプラグインを作っておけば、アプリケーションを実行した環境によってプラグインを切り替えるというようなことも可能です。

(5)保守性が高い

後に説明しますが、プラグインのインタフェースはどのプラグインでも同じになります。そのため、保守に際してどの関数をメンテナンスしていいのかが、比較的わかりやすくなります(ただし、目的の異なるプラグインの場合はインタフェースが異なります。詳しくは後述の5章5節を参照してください)。

2.3. プラグインを使うデメリット

プラグインを使うことで次の3つのデメリットが考えられます。この3つのデメリットは、立場の違い(アプリケーション開発者かプラグイン開発者)によって解釈の仕方が異なります。

(1)アプリケーション開発者にとってのデメリット

(a)プラグインが必要としそうなパラメータを与える。

(b)プラグインが必要としそうな関数を用意する。

(c)プラグインにどこまでさせるのかを決める必要があります。

(2)プラグイン開発者にとってのデメリット

(a)必要なパラメータが得られない(得にくい)。

(b)必要な関数がない(初期化などのイベント的な関数)。

(c)特になし

プラグイン開発者の(c)に関していえば、アプリケーション開発者が不要だと思っている機能をプラグイン開発者が開発する必要はないため「特になし」となっています。あるいは、(b)に分類されるかもしれません。

これらのデメリットは、乱暴にまとめてしまえば「プラグインに対する仕様の決定が難しい」だけだといえます。

3. プラグインの種類

プラグイン対応アプリケーションの実現方法には、大きく2つの方法があります。

3.1. スクリプトタイプのプラグイン

目的とする機能を実現するのに必要な手続きをテキストに記述し、アプリケーションが実行時にそのテキストを読み込み、逐次実行していくタイプのプラグインです。スクリプトタイプのプラグインを実現するには、まずスクリプト言語の仕様を定義します。プラグイン開発者は、このスクリプト言語でプラグインを開発することになります。アプリケーションは、プラグイン開発者が記述したスクリプトを読み込み、その内容を実行します。単に「実行」といっても、字句解析や構文解析、実際のコード生成などが必要になってきます。

本ドキュメントでは、このスクリプトタイプのプラグインは扱いません。このタイプのプラグインに関しては、「スクリプト言語の作り方」のようなページを参考にするといいと思います。

3.2. DLLタイプのプラグイン

DLL(Dynamic Link Library、動的リンクライブラリ)は、アプリケーションの実行時にリンクされるライブラリです。逆にアプリケーションのビルド時にリンクされるライブラリは「Static Link Library(静的リンクライブラリ)」です(SLLとは呼ばれていません)。

動的リンクライブラリは、アプリケーションとライブラリが独立しているため、後々にライブラリのみを入れ替えることが可能であったり、複数のアプリケーションで共有化することができます(図2)。また、アプリケーション自体のサイズも小さくなります。

静的リンクライブラリは、ビルド時にアプリケーションとリンクされるため、図3のように、アプリケーション実行時にライブラリが存在することが保証されます。

 |----------------------|   |------------|
 |            |   |            |
 |            |   |            |
 | アプリケーション本体 |---| ライブラリ |
 |            |   |            |
 |            |   |            |
 |----------------------|   |------------|
   ( 図2:動的リンクの概念図 )
 |----------------------|------------|
 |            |            |
 |            |            |
 | アプリケーション本体 | ライブラリ |
 |            |            |
 |            |            |
 |----------------------|------------|
   ( 図3:静的リンクの概念図 )

動的リンクライブラリを使用するには、2つの方法があります。

(1)暗黙的リンク

動的リンクライブラリにある関数を使ったソースコードをコンパイルするとアプリケーション外部の関数を呼び出しているわけですから、オブジェクトコード内に外部関数への参照が生成されます。もちろん、このままでは「未解決の〜〜」などというエラーが発生します。

暗黙的リンクでは、外部参照を解決する手段として、インポートライブラリというものを使います。インポートライブラリには外部参照した関数の確認に必要な情報が提供されています(詳細は省きます)。

一見、インポートライブラリをビルド時に指定するため、あたかもビルド時に必要な外部参照のコードがアプリケーションにリンクされてしまっているように見えます。しかしインポートライブラリ自体には外部参照した関数のコードは含まれていません。あくまで“実行時に”外部参照を解決するコードが自動的に埋め込まれただけです。

(VCでは、プロジェクトの設定でインポートライブラリを指定します。VCでは、C標準ライブラリなどを解決するインポートライブラリが自動的にリンクされます。)

(2)明示的リンク

明示的リンクではアプリケーション開発者が外部参照を解決するためのコードを埋め込む必要があります(Windows APIを使います)。このためアプリケーション開発者は、自分の必要なライブラリを必要なときにリンクすることができます。

プラグイン対応アプリケーションでは、この明示的リンクを使う必要があります(実行するまで拡張機能があるかどうかもわからないので)。また明示的リンクでは、インポートライブラリを必要としません。

両者の違いをまとめると、以下のようになります。

暗黙的リンクと明示的リンクの違い
外部参照解決方法解決されるタイミング
暗黙的リンク開発環境が自動に行ってくれるアプリケーション起動時
明示的リンク開発者が明示的に指示する開発者の指示したタイミング

* 通常、静的リンクライブラリの拡張子もインポートライブラリの拡張子も「.lib」ですが、両者の違いは明白です。静的リンクライブラリには、ライブラリ関数のコードが含まれていますが、インポートライブラリには実際のコードは含まれていません。インポートライブラリは、ただDLL関数への外部参照を解決するための情報を持っているだけです。

4章では、サンプルプログラムを作りながら、DLLタイプのプラグインを使ったアプリケーションの実現方法を見ていきます。

4. DLLを使ったプラグイン対応アプリケーションの実現方法(C版)

本章ではサンプルとして簡単な仕様のプラグイン対応アプリケーションを開発していきます。

サンプルプログラムの仕様
対象C言語(またはC++言語)を使ってWin32プログラミングをしたことがある方。
開発環境
テスト環境
OS:Microsoft Windows XP Professional SP2
開発ソフト:Microsoft Visual Studio.NET 2003
開発言語:Visual C++ with Win32 API

4.1. 必要になるWin32 API

今回重要な役割を果たすのが次のAPIです。

LoadLibrary API
プロトタイプ宣言HMODULE LoadLibrary(
    LPCTSTR lpFileName // モジュールのファイル名
);
内容指定された実行可能モジュールを呼び出し側プロセスのアドレス空間内にマップします。
戻り値関数が成功するとモジュールのハンドルが返ります。失敗するとNULLが返ります。
GetProcAddress API
プロトタイプ宣言FARPROC GetProcAddress(
    HMODULE hModule, // DLL モジュールのハンドル
    LPCSTR lpProcName // 関数名
);
内容DLLが持つ、指定されたエクスポート済み関数のアドレスを取得します。
戻り値関数が成功するとDLLのエクスポート済み関数のアドレスが返ります。失敗するとNULLが返ります。

4.2. サンプルプログラムの目的

ある計算の結果を求めるプログラムを考えます。

この計算には、答えを求める方法が複数あるとします(たとえば「円周率を求める計算」や「擬似乱数を求める計算」などを想像してもらえれば良いかと思います)。

このプログラムでは、後から計算方法を修正したり、あらたな手法で計算結果を求めたりすることが考えられるので、具体的に計算を行う部分をプラグイン化することにします。

この計算には、次の3つの条件があるものとします。

プラグインを使わないアプリケーションの場合、以下のような関数を作ることになる思います。

/* 計算結果を求める */
int Calc(int param1, int param2)
{
  int result; /* 計算結果を格納する */

  /* 略 */

  return result;
}

この関数を複数の計算方法で切り替えたいとするなら、次のような関数を作ることになると思います。

/* 計算結果を手法「AAA」によって求める */
int CalcByAAA(int param1, int param2);

/* 計算結果を手法「BBB」によって求める */
int CalcByBBB(int param1, int param2);

/* 計算結果を手法「CCC」によって求める */
int CalcByCCC(int param1, int param2);

/* ある問題の答えを求める */
int Calc(int param1, int param2);

4.3. プラグインの仕様

アプリケーションは、次のようにただ関数「Calc()」を呼ぶだけで、その中身については知らなくても計算結果を得られるようにします。つまり、関数「Calc()」の実装部分をプラグインによって、実行時に切り替えられるようにします。

つまり、プラグイン開発者は必ず関数「Calc()」を定義します。

/* ある問題の答えを求める */
int Calc(int param1, int param2);

以上がプラグインの仕様です。今回は簡単な例にとどめていますが、実際にはプラグイン自体の初期化処理や終了処理を行うための関数が必要であったり、プラグインの名前やバージョンを知るための方法などがあった方が良いかと思います。

4.4. アプリケーションの作成

ここでは、Win32コンソールアプリケーションを使って、プラグイン対応アプリケーションを作成します。もちろんコンソールアプリケーションである必要はありません。

(1)新しいプロジェクトの作成します。

Visual C++を起動して、「新しいプロジェクト」(Ctrl+Shift+N)を作成します。プロジェクトの種類を[Visual C++プロジェクト]の[Win32]にし、テンプレートから[Win32プロジェクト]を選びます。

適当にプロジェクト名を設定し、OKボタンを押します。「Win32アプリケーションウィザード」にて以下のように設定し、OKボタンを押します。

|=============================================================
|------[概要]
|------[アプリケーションの設定]
|   |------[アプリケーションの種類]
|   |    |------ "コンソールアプリケーション" にチェック
|   |
|   |------[追加のオプション]
|        |------ "空のプロジェクト"にチェック
|
|=============================================================

(2)アプリケーションの核となるソースファイルを追加します。

「新しいファイルを追加」(Ctrl+N)にて、[C++ファイル(cpp)]を追加します。

(3)必要なヘッダをインクルードします。

Win32 APIが必要となりますので、[windows.h]をインクルードします。

#include <windows.h>

(4)プラグインで実装される関数の関数ポインタを定義します。

プラグインの関数を呼び出す際に、呼び出す関数の関数ポインタがあると便利ですので、前もって定義しておくことをおすすめします。

typedef int (*API_Calc)(int param1, int param2);

(5)main関数を定義します。

int main(void)
{
  return 0;
}

(6)プラグインのハンドルを用意します。

HMODULE hmod;

(7)必要なプラグインをロードします。

ここでは、LoadLibrary()というWindowsのAPIを呼び出しています。この関数は、DLLやリソースファイルなどのモジュールを実行時に呼び出すための関数です。引数には、呼び出したいモジュールのファイル名(パス)を与えます。

hmod = LoadLibrary("MyPlugin.dll");

(8)プラグインに定義されている関数を読み込みます。

プラグインに定義されている関数(以降「プラグイン関数」と呼ぶ)を使うためには、GetProcAddress() APIを使います。

この関数の第1引数に(7)で得たプラグインハンドルを渡し、第2引数にはプラグインに必要な関数名を記述します。引数に無効なハンドルや存在しない関数名を与えた場合、GetProcAddress() は、NULLを返します。

GetProcAddress()を以下のように関数の型にキャストし、関数ポインタ変数に代入することで、関数が使えるようになります(API_Calcは(4)でtypedefしたものです)。

API_Calc Calc;
Calc = (API_Calc)GetProcAddress(hmod, "Calc");

(9)関数を使用します。

ロードした関数は、普通の関数と同じように使います。

(事前に「Calc != NULL」であることをチェックしてください)

cout << "Result: " << Calc(56, 7) << endl;

(10)プラグインを解放します。

ロードしているプラグインを使用し終わったら、プラグインを解放します。プラグインを解放すると、ロードしていた関数も使えなくなります。

FreeLibrary(hmod);

以上で、プラグイン対応アプリケーションは完成です。

複数のプラグインを読み込み、それらを切り替える方法については、5章2節で説明します。

次はプラグイン側のプログラムを開発します。

4.5. プラグインの開発

プラグイン側のプログラムを作ります。

(1)新しいプロジェクトの作成します。

Visual C++を起動して、「新しいプロジェクト」(Ctrl+Shift+N)を作成します。プロジェクトの種類を[Visual C++プロジェクト]の[Win32]にし、テンプレートから[Win32プロジェクト]を選びます。

適当にプロジェクト名を設定し、OKボタンを押します。「Win32アプリケーションウィザード」にて以下のように設定し、OKボタンを押します。

|=============================================================
|------[概要]
|------[アプリケーションの設定]
|   |------[アプリケーションの種類]
|   |    |------ "DLL" にチェック
|   |
|   |------[追加のオプション]
|        |------ "空のプロジェクト"にチェック
|
|=============================================================

(2)モジュール定義ファイル(.DEF)の追加します。

モジュール定義ファイルは、DLLの外部に公開する関数を設定するファイルです。モジュール定義ファイルを追加するには、[新しい項目の追加(Ctrl+Shift+A)]で[DEFファイル(def)]を選び、ファイル名を指定し [OK]ボタンを押します。

モジュール定義ファイルの中身は、以下のように記述します。

; 行頭にセミコロンを書くと、その行がコメントになります。

; この定義ファイルで各種情報を設定するDLLの名前
; 複数のDLLを出力する場合に、DLL名によって区別する。
LIBRARY MyPlugin

; エクスポートする関数の設定
EXPORTS
;   [エクスポートする関数名] (@序数)
;   序数は、外部から関数名でなく序数でアクセスするための数字。
;   序数は省略可能
  Calc @1

このようにDLLでは、DLLの外部に公開する関数をモジュール定義ファイルに記述しますが、すべての関数を外部に公開する必要はありません。

以降では、外部に公開した関数のことを「エクスポート関数」と呼ぶことにします。

モジュール定義ファイルについてのもう少し詳しい情報は、5章1節で説明します。

(3)プラグインの核となるソースファイルを追加します。

「新しいファイルを追加」(Ctrl+N)にて、[C++ファイル(cpp)]を追加します。

(4)必要なヘッダをインクルードします。

特に必要なヘッダはありませんが、もしアプリケーション開発者がプラグイン開発者向けに公開しているヘッダ(プラグイン関数のプロトタイプ宣言などがされている)があるのであれば、それをインクルードした方が良いでしょう。

(5)プラグインの仕様を満たす関数群を定義します。

プラグインに必要な関数を定義します。

今回のサンプルでは、Calc()関数のみが必要です。

もちろん、Calc()以外の関数も定義可能です。

int Calc(int param1, int param2)
{
  return param1 + param2;
}

(6)ビルドして、完成です。

これでプラグインは完成です。ビルドでエラーが出る場合は、以下の点を見直してください。

(a)モジュール定義ファイルに記述しているエクスポート関数はすべて定義・実装しているか

(b)プロジェクトの構成プロパティの[リンク]-[モジュール定義ファイル]の値が自分で追加したモジュール定義ファイルになっているか

4.6. アプリケーションの実行

実行前にプラグインのDLLファイルを以下のいずれかのフォルダに置きます。以下のフォルダに目的のDLLが内場合、LoadLibrary()は失敗します。

(1)アプリケーションのロード元ディレクトリ

(2)カレントディレクトリ

(3)Windowsシステムディレクトリ(C:\System32)

(4)Windowsディレクトリ(C:\Windows)

(5)環境変数PATHに記述されている各ディレクトリ

(1)は、よくわかっていませんが、恐らくLoadLibrary()の引数にフルパスを指定したときのディレクトリのことだと思います。

5. 落ち穂拾い

4章で紹介しきれなかった部分を掻い摘んで紹介します。

5.1. モジュール定義ファイルについて

モジュール定義ファイルを使わなくても、DLLを作ることができます。

しかし、モジュール定義ファイルを使うことで、次の利点が得られます。

(1)余計なプリフィックス/サフィックスがつかない。

モジュール定義ファイルを使わない場合、関数名の前後にユニークな識別子が付加されて関数がエクスポートされます(「?Function@@YAHHH@Z」など)。そのため、アプリケーションからDLLにアクセスする際に余計な文字列を埋め込む必要が出て、保守が難しくなります(プラグインの仕様が変わるたびにプリフィックス/サフィックスが変わる)。

(2)ソースファイル内の関数名を自由に決めることができる。

プラグインのソースファイル内で定義した関数名とは違う名前で外部にエクスポートできます。たとえば、プラグイン開発時には「Func」という名前で定義した関数を、エクスポートする際には「Function」にしたいという場合は、以下のようにモジュール定義ファイルに記述することで左辺値に指定した名前でエクスポートできるようになります。

LIBRARY MyPlugin
EXPORTS
;   左辺値:実際にエクスポートされる関数名
;   右辺値:コード上の関数名
  Function = Func

5.2. 複数のプラグインのロード

複数のプラグインを読み込む場合、以下のようにします。

(1)複数のハンドル

プラグインを区別するために、ハンドルは複数用意します。

HMODULE plugin[MAX_PLUGIN];

(2)複数の関数ポインタ変数

定義した関数ポインタの変数も複数用意します。

API_Calc Calc [MAX_PLUGIN];

また次のように1つのプラグインに必要な情報を構造体としてまとめておきます。こうすることでプラグインの数が増えた際などの拡張性や保守性が高まります。

struct MyPlugin
{
  HMODULE        hmod;
  API_Calc       Calc;
  API_Init       Init;
  API_GetName    GetName;
  API_GetVerNum  GetVerNum;
};

(3)プラグイン、関数のロード、解放

複数のプラグインや関数のロード/解放は、LoadLibrary()などを使って1つずつロード/解放していきます。

MyPlugin plugin[MAX_PLUGIN];
for (int i = 0; i < MAX_PLUGIN; i++) {
  plugin[i].hmod = LoadLibrary(filepath[i]);
  plugin[i].Calc = (API_Calc)GetProcAddress(hmod, "Calc");
  plugin[i].Init = (API_Init)GetProcAddress(hmod, "Init");
  /* 略 */
}
/* 略 */
for (int i = 0; i < MAX_PLUGIN; i++) {
   FreeLibrary(plugin[i].hmod);
}

5.3. 読み込むプラグインのファイル名取得方法

普通、プラグインを読み込む際には、(そもそも後から追加されるのですから)プラグインの入っているDLLのファイル名をビルド時に知ることはできません。

そこで、プラグインのファイル名を知るために、Win32 APIのFindFirstFile()とFindNextFile()を使用してファイル名を1つずつ取得します。詳細は割愛しますが、以下のコードで「./plugin」以下にある拡張子「.dll」のファイルの一覧が表示されます。

WIN32_FIND_DATA w32findData;
HANDLE hFind;
/* pluginフォルダ直下の「.dll」ファイルを検索する */
hFind = FindFirstFile(".\\plugin\\*.dll", &w32findData);
if (hFind == INVALID_HANDLE_VALUE) {
  std::cout << "There is no plugin." << std::endl;
}

do {
  /* 検索パターンにマッチするファイル名を出力する */
  std::cout << w32findData.cFileName << std::endl;

  /* 次のファイルを検索する。存在しなければ、戻り値はfalseとなる */
} while( FindNextFile(hFind, &w32findData) );

/* 検索を終了する */
FindClose(hFind);

複数プラグインを読み込む場合は、一度だけ上記の操作を行いプラグインファイル名のリストを持っておくといいと思います。

5.4. プラグインの拡張子

読み込むDLLの拡張子が[.dll]である必要はありません。[.plugin]でも何でもかまいません。

5.5. プラグインの種類

プラグインは、目的にあわせて仕様を規定するべきです。たとえば、4章のサンプルでは計算を行うプラグインを作成しました。これからさらに計算結果を表示するプラグインを作成するとします。

このときに表示する機能と計算する機能は、本質的に関係のないものです。それならば、表示するプラグインには表示に関係する仕様を定義し、計算するプラグインには計算に関するプラグインを定義するべきです。

仕様を機能ごとに分離することで、プラグイン開発者は無駄な関数を定義する必要がなくなり、プラグイン自体のデータ容量も抑えることが可能になります。

プラグインの種類を実行時に識別するには、以下の3つの方法があります。

(1)種類ごとにフォルダを分ける。

(2)種類ごとに拡張子を変える。

(3)ある関数が定義されているかで調べる。

5.6. プラグインからアプリケーションの関数を呼ぶ方法

プラグインからアプリケーションの関数を呼ぶことが可能です。

方針は、プラグイン側に関数ポインタを渡せるような関数を用意するだけです。アプリケーションは、その関数によって、プラグインから呼ばれる関数をセットします。

■サンプルコード:アプリケーション側

void AppFunc(int val)
{
}

/* 略 */
PlugFunc(AppFunc);

■サンプルコード:プラグイン側

typedef void (*AppFunc)(int val);

void PlugFunc(AppFunc appfunc)
{
  appfunc(2);
}

5.7. プラグインのエントリポイント

プラグインがプロセスにロードされた時点で何らかの処理を行いたい場合、「DllMain」という関数を定義します。また、プロセスから破棄されようとしている場合にも呼び出されます。

 |--------------------------------------------------------|
 | BOOL WINAPI DllMain(                                   |
 |   HINSTANCE hinstDLL  // DLLモジュールのハンドル       |
 |   DWORD fdwReason,    // 関数を呼び出す理由            |
 |   LPVOID lpvReserved  // 予約済み                      |
 | );                                                     |
 |--------------------------------------------------------|

詳細は、プラットフォームSDK「DllMain」を参照してください。

5.8. クラスのプラグイン化

正確にいうと、ここで紹介する方法は「クラスのプラグイン化」ではなく、「プラグインにクラスを含める方法」です。ただ、実質的にはクラスをプラグイン化しています。

クラスをプラグインに含めるだけなら、何も考えずに行うことが可能です。しかしクラスをエクスポートする場合には面倒になります。そもそもモジュール定義ファイルにクラス名を記述できませんし、記述できたとしても関数ポインタに相当する機能がC++にはありません。

(関数の型情報を持つ型が関数ポインタであるなら、クラスの型情報を持つ型としてクラスポインタがあってもいいのですが、普通「クラスポインタ」といえば、あるクラスのインスタンスへのポインタを指します。宣言するようなものは、エクスポートできないことになります。)

しかしクラスをエクスポートするのではなく、クラスのインスタンスを作成する関数をエクスポートするのであれば、クラスを間接的にエクスポートすることが可能です。

多少、面倒ですが、クラスをエクスポートするには、次の利点があります。

(1)関数名の衝突を考えずに済む。

クラス内の関数メンバは、クラススコープに入っているため、オーバーロードも可能です。

(2)プラグインの拡張・保守がしやすくなる。

グローバルスコープに関数を並べていくだけでは、プラグインの拡張や保守が段々と難しくなってきます。たとえば複数種のプラグインがある場合に、種類毎にクラスとして扱うことができます。

種類の区別を、クラスのインスタンスを作成する関数がエクスポートされているかどうかで判断することが可能です。

具体的なクラスのプラグイン化については6章で説明します。

6. プラグインクラスの実現方法

本章では、クラスをプラグイン化するための方法を説明します。

クラスを含んだプラグインのことを以降では「プラグインクラス」と呼ぶことにします。

6.1. 実現方法の概要

プラグインクラスの実現には、デザインパターン「FactoryMethod」を用います。つまりアプリケーションでは、プラグインクラスのインスタンスを作らずに、プラグイン内で作られたインスタンスを利用します。

 簡単な例を見てみます(実際にはビルドエラーになります)。

■サンプルコード:アプリケーションの公開ヘッダ

class MyClass
{
public:
  MyClass(void);
  const char* GetName(void);
};

MyClass* MakeInstance(void);

■サンプルコード:プラグイン内

/* 公開ヘッダをincludeしておくこと */
MyClass::MyClass(void) {}
const char* MyClass::GetName(void) {return "MyPlugin";}

MyClass* MakeInstance(void) {
  return new MyClass();
}

■サンプルコード:アプリケーション

/* プラグインクのMakeInstance()をあらかじめロードしておきます */
MyClass* instance = MakeInstance();
cout << instance->GetName() << endl;
delete instance;

公開ヘッダでは、MyClassの定義のみを行い、実装を行いません。

プラグインでは、MakeInstance()関数のみをエクスポートするようにします。

アプリケーションでは、プラグインからロードしたMakeInstance()関数のみを利用し、直接MyClassのインスタンスを作らないようにします。

このように、実際にはクラスをプラグイン外部に公開しなくても、あるエクスポートされた関数を使って特定のクラスを生成・解放することでプラグインクラスを実現できます。

6.2. プラグインクラスのサンプル

本節では、簡単なサンプルプログラムを用いて、プラグインクラスの実現方法を説明します。

(1)公開ヘッダの作成

プラグインクラス内の関数メンバはすべて仮想関数である必要があります。

またコンストラクタ・デストラクタはprotectedに指定します。これはアプリケーションが直接プラグインクラスのインスタンスを作れなくするためです。こうする理由は、6章3節にて説明します。

インスタンスを作るには、プラグイン側で定義するインスタンス生成/解放を行う専用の関数を使います(前節で説明しました)。この関数は、プラグインクラスのフレンド関数に指定することで、プラグインクラスのprotectedなコンストラクタ/デストラクタを呼び出すことを可能にします。

class MyPluginClass;

MyPluginClass* MakeInstance(void);
void ReleaseInstance(MyPluginClass* instance);

class MyPluginClass
{
protected:
  explicit MyPluginClass(void);
  virtual ~MyPluginClass(void);

public:
  virtual float GetVersion(void);

  friend MyPluginClass* MakeInstance(void);
  friend void ReleaseInstance(MyPluginClass* instance);
};

(2)プラグインの実装

プラグインクラスの実装と、インスタンスの生成と解放を行う関数の実装を行います。インスタンスの生成と解放を行う関数の定義は公開ヘッダで行っても構いません。

/* MyPluginClassのインスタンスを作る */
MyPluginClass* MakeInstance(void)
{
  return new MyPluginClass();
}

/* MyPluginClassのインスタンスを解放する */
void ReleaseInstance(MyPluginClass* instance)
{
  delete instance;
}

MyPluginClass::MyPluginClass(void)
{
}

MyPluginClass::~MyPluginClass(void)
{
}

float MyPluginClass::GetVersion(void)
{
  return 1.03f;
}

(3)モジュール定義ファイルの追加

公開する関数を規定します。

ここでは、MakeInstance()とReleaseInstance()のみを公開します。

LIBRARY  MyPlugin1
EXPORTS
  MakeInstance    @1
  ReleaseInstance @2

(4)アプリケーションでのプラグインロード

プラグインのロードや、エクスポート関数をロードする方法は、4章4節と同じ方法で行います。特に注意すべき点はありません。

/* 公開ヘッダをインクルードしておきます */

typedef MyPluginClass* (*API_MakeInstance)(void);
typedef void (*API_ReleaseInstance)(MyPluginClass* instance);

HMODULE hmod;
/* プラグイン内の関数へのポインタ */
API_MakeInstance MakeInstance;
/* プラグイン内の関数へのポインタ */
API_ReleaseInstance ReleaseInstance;

/* プラグインと関数のロード */
hmod  = LoadLibrary("Plugin.dll");
MakeInstance = (API_MakeInstance)GetProcAddress(hmod, "MakeInstance");
ReleaseInstance = (API_ReleaseInstance)GetProcAddress(hmod, "ReleaseInstance");

(5)アプリケーションでのプラグインクラスのロードと使用

プラグインで定義されているクラスをロードします。ロードといっても実際には、プラグインで定義されているMakeInstance()関数を使って、目的のクラスのインスタンスを取得するだけです。

クラスのインスタンスは、特に注意することなく普通のクラスとして扱えます。また、MakeInstance()で得たインスタンスは、必ず解放しなければなりませんが、deleteを使ってはいけません(MakeInstance()でnewして取得したものだとは限らないからです。詳しくは6章3節を参考にしてください)。

MyPluginClass* mpc; /* プラグイン内のクラスへのポインタ */
mpc = MakeInstance();
cout << "Version: " << mpc->GetVersion() << endl;
ReleaseInstance(mpc);

(6)アプリケーションでのプラグイン解放

これも4章4節と同じ方法で行います。特に注意すべき点はありません。

FreeLibrary(hmod);

6.3. プラグインクラスの落ち穂拾い

プラグインクラスの実現に関して、注意すべき点を書きます

(1)メンバを拡張する。

プラグインを開発する際に、公開ヘッダに書かれてあるクラスのメンバはすべて定義すべきです。定義しない場合は、ビルドエラーとなります(だからといって公開ヘッダを編集してはいけません)。

また、公開ヘッダを編集して独自にクラスのメンバを増やすことも可能ですが、公開ヘッダを編集せずに、次項「クラスを継承する」で対応すべきです。

(2)クラスを継承する。

公開ヘッダに定義されるクラスを継承して、メンバを拡張することが可能です。

(3)クラスが継承される可能性を考える。

アプリケーション開発者は、公開ヘッダに定義するクラスが継承される可能性があることを考慮に入れなければなりません(デストラクタを仮想関数にする理由もそこにあります)。

クラスが継承される可能性を考えるのであれば、クラスを純粋仮想関数で構成するのがいいかもしれません。

(4)クラスのサイズは扱わない。

アプリケーション側でクラスのサイズを「sizeof(MyPluginClass)」のように取得することは危険です。これは公開ヘッダに書かれたクラスのサイズと、プラグインで拡張定義されたクラスのサイズが異なるかもしれないからです。

たとえば、ありがちなのがmalloc()やmemcpy()を使ってクラスのインスタンスを初期化・コピーしようとした場合です。そもそもmalloc()を使うとコンストラクタが呼ばれませんし、memcpy()でクラスをコピーできるとも限りません(2つのクラスのメンバが同じデータのアドレスを指していたら、予期しない動作をしてしまいます)。

(5)クラスの生成・解放はプラグイン側で行うようにする。

前項で、もう1つ忘れてはいけないのが、new/delete演算子によるインスタンスの生成/解放のことです。new演算子は、MakeInstance()によって隠蔽されているので問題ありませんが、delete演算子は危険です(6章1節のサンプルは危険です)。

通常delete演算子は解放したいクラスのデストラクタを呼んで、さらに解放したいクラスのインスタンスをfreeします。このときクラスのインスタンスのサイズ分だけfreeするのですが「インスタンスのサイズ」というのは、アプリケーションとプラグインで異なる恐れがあるのです。

そのため、new演算子がMakeInstance()に隠蔽されたように、delete演算子をプラグイン側で隠蔽してやります。たとえばdeleteはReleaseInstance()という関数で行うようにすれば、クラスのインスタンスが確保されるのも、解放されるのもプラグイン側で行われるようになり、結果として、インスタンスのサイズが食い違わなくなります。

7. サンプルプログラム

今回作成したサンプルプログラムのアーカイブをダウンロード可能です。すべて7-zip圧縮です。

サンプルプログラムの内容
章番号サンプル名内容
4章C4_SimplePluginAppli.7z単純なサンプル
5章1節C5_FreeFunctionNameByDefFile.7zDEFファイルで関数名変更
5章2節C5_SwitchPlugin.7z複数プラグイン
5章3節C5_ListUpPlugin.7zファイルリストを作る
5章6節C5_TheFunctionCalledFromPlugin.7zプラグインから呼ぶ関数
6章C6_PluginClass.7zプラグインクラス
6章3節C6_PluginInterface.7zプラグインインタフェース

8. 参考資料

|=============================================================
|------[概要]
|------[アプリケーションの設定]
|   |------[アプリケーションの種類]
|   |    |------ "DLL" にチェック
|   |
|   |------[追加のオプション]
|        |------ "シンボルのエクスポート"にチェック
|
|=============================================================

Comments are welcome! We can be reached at whoinside_reshia@hotmail.co.jp
2005/06/30 12:50:30 UTC