よくあるDLLを使ったプラグインのような動作をしてみたかった


プラグインは概念はわかってるんだけど、プログラムにするときどうすりゃいいの?って思ったので調べた。
C++の方が継承を使ってそれっぽく出来るので、C++で設計。
クラスの設計むずかしい;w;
作り方がわかればいいので、Windows環境前提でちまちま作った。


参考になった所。

安全な DLL の中にあるC++ のクラスのエクスポート (DLLを前提にしたクラス設計)
Tips - DLL (MinGWでDLL生成するに当たって)
http://ura5han.resourcez.org/wiki/MinGW/dll%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%99 (WindowsのDLLを読み込むときに)

開発環境
Windows XP + MinGW GCC(g++ 4.5.0, dlltool 2.20.51)


青果物Output
http://rying.net/arc/example_plugin.zip

設計

まずはインターフェースです。
といっても、何か凄い事をするわけでもないので、
とりあえず"バージョン情報を返す"操作だけ実装します。

PluginInterface.hpp
#ifndef _PLUGIN_INTERFACE_HPP_
#define _PLUGIN_INTERFACE_HPP_

// windowsにはdlfcn.hがないのと、デフォルトではキャストがめんどうなので、マクロで黙らせる
#include <windows.h>
#define dlopen(P,G) (void*)LoadLibrary(P)
#define dlsym(D,F) (void*)GetProcAddress((HMODULE)D,F)
#define dlclose(D) FreeLibrary((HMODULE)D)

class PluginInterface{
public:
	virtual const char * version() = 0; // バージョン情報を文字列で返す関数
	virtual ~PluginInterface(){}; // 何も定義してないから開放するものも当然無い。だが継承先のデストラクタを呼び出すのに必須
};

#endif

インターフェースなので、具体的な処理を書きません。
ただ、バージョン情報を返すメソッドversionを純粋仮想関数として定義しています。
このクラスを継承したら、必ずversionメソッドしてあげる必要があります。
デストラクタは、何も定義して無いクラスなので、やる事は無いです。

実装

設計は終わりました。次に実際に処理を書いていきます。
PluginSampleクラスをPluginInterfaceクラスを継承して作ります。

PluginSample.cpp
#include "PluginInterface.hpp"
#include <iostream>

class PluginSample : public PluginInterface{
private:

public:
	PluginSample(){
		std::cout << "PluginSample constract" << std::endl;
	}
	~PluginSample(){
		std::cout << "PluginSample destract" << std::endl;
	}
	
	const char * version(void){
		return "PluginSample Version 0.1";
	}
};

extern "C" {
	PluginInterface * Create(void){ return new PluginSample(); }
}

なんとなくコンストラクタとデストラクタに文字を出力させてます。
versionメソッドにも、バージョン情報を示す文字列定数を返すようにしています。
extern "C"の部分は、C言語として解釈させるブロックを示します。(そのくせnewを使ってます。大丈夫なのかな?)
extern "C"をしないといけない理由はよくわかんないですが、Create関数が外部に公開(EXPORT)する関数です。
今回はプラグインが持つクラスのインスタンスを生成する意味で"Create"と名前をつけました。
コレにアクセスしてクラスのインスタンスを作ってもらいます。

PluginSample.def
EXPORTS
 Create

定義ファイルです。よくわかりません。
先ほどextern "C"で括った部分の関数名を書いておきます。


これでプラグインの関数定義はおしまいです。
この調子で2個ほど作っておきます。
PluginSampleEx.cpp/def ..... PluginSampleを拡張したプラグインに見せたダミープラグイン
PluginSampleDefect.cpp/def ..... 欠陥プラグイン(Createの代わりにcreateObjectをEXPORT関数に定義)

プラグインを使うプログラム

実際にプラグインを使うプログラムを書いていきます。

UsePlugin.cpp
#include "PluginInterface.hpp"

#include <string>
#include <iostream>
#include <vector>
#include <memory>

int main(int argc, char**argv){
	std::vector<std::string> plugins; // プラグインたち
	std::vector<void *> handles; // DLLのハンドルを記憶
	
	// 何らかの方法で使うプラグインを列挙。文字列でいいのでファイルや引数からでもOK
	plugins.push_back("HogeHoge.dll");
	plugins.push_back("PluginSampleDefect.dll");
	plugins.push_back("PluginSample.dll");
	plugins.push_back("PluginSampleEx.dll");
	
	
	// プラグインを読み込む
	for(int i=0; i<plugins.size(); ++i){
		void *handle;
		PluginInterface * (*create)(void); // 各プラグインのCreate関数の位置を記憶する関数ポインタ
		
		std::cout << "--------------------" << std::endl;
		
		// 同名のDLLがあるか探す
		handle = dlopen (plugins[i].c_str(), "");
		if(handle == NULL){
			std::cout << "Can't load Plugin: " << plugins[i] << std::endl;
			continue;
		}
		
		// DLL内にCreate関数があるか探す
		create = (PluginInterface *(*)(void)) dlsym(handle, "Create"); // 関数ポインタに合うようにでキャスト
//		*(void **)&create = dlsym(handle, "Create"); // 上の変わりにこっちでもいい
		
		if(create == NULL){
			std::cout << "Not found 'Create' Function: " << plugins[i] << std::endl;
			dlclose(handle); // 使わないプラグインだと思うのでDLLをしっかり開放
			continue;
		}
		std::cout << "Found 'Create' Function: " << plugins[i] << std::endl;
		
		handles.push_back(handle); // 使用するDLLのハンドルをvectorへ記憶しておく。
		
		// interfaceを使ってプラグインを使用
		std::auto_ptr<PluginInterface> p(create()); // pluginのクラスをcreateする。
		std::cout << p->version() << std::endl; // とりあえずversion表示だけ
		// auto_ptrのスコープを抜けるので、デストラクタが呼び出されるはず。
	}
	
	// DLLの開放
	for(int i=0; i<handles.size(); ++i){ dlclose(handles[i]); handles[i] = NULL; }
}

プラグインを読み取って、バージョン情報を表示するだけのプログラムです。
インターフェースの情報はヘッダファイルPluginInterface.hppから読み取っていますが、プラグインのクラス定義は一切ありません。
インターフェースで定義されたversionメソッドと、インターフェイスに則って作られたクラスであれば、どのプラグインも"Create"関数を通してインスタンスを生成できますし使うことができます。
もっと色々やるのであれば色々な操作を持たせるべきでしょう。

コンパイルとか

DLLの生成には、MinGWのdlltoolを使います。
複数あって1個づつコンパイルするのは大変なので、Makefileを作ります。

UsePlugin.mak
CXX= g++
CFLAGS = -O2
LIBS =

EXECUTE = UsePlugin.exe
PLUGINS= PluginSample.dll PluginSampleEx.dll PluginSampleDefect.dll 
PLUGIN_INTERFACE = PluginInterface.hpp


all: $(PLUGINS) $(EXECUTE)
	
clean:
	del /Q *.dll *.a *.exe *.o *.tmp *.exp

$(EXECUTE):
	${CXX} ${CFLAGS} -o $@ $(@:%.exe=%.cpp)
	
$(PLUGINS): 
	${CXX} ${CFLAGS} -c -o $(@:%.dll=%.o) $(@:%.dll=%.cpp)
	${CXX} -mdll -o junk.tmp -Wl,--base-file,base.tmp $(@:%.dll=%.o)
	dlltool -l lib$(@:%.dll=%).a --dllname $@ --base-file base.tmp --output-exp temp.exp --def $(@:%.dll=%.def)
	${CXX} -mdll -o $@ $(@:%.dll=%.o) -Wl,temp.exp
	del /Q *.tmp
	del /Q *.exp
	del /Q *.o

Makefileの作り方がよくわからないので適当に書いたら、1度makeが終わったらdllとexeを消さない限り再度makeできないようなmakefileになってしまいました。
依存関係とかよくわかんない。

実行してみる。

                                      • -

Can't load Plugin: HogeHoge.dll

                                      • -

Not found 'Create' Function: PluginSampleDefect.dll

                                      • -

Found 'Create' Function: PluginSample.dll
PluginSample constract
PluginSample Version 0.1
PluginSample destract

                                      • -

Found 'Create' Function: PluginSampleEx.dll
PluginSampleEx constract
PluginSampleEx Version 1000000000
PluginSampleEx destract

それっぽく動いてますね。
UsePlugin.exeの階層だけ変えて実行すると、こうなります。

                                      • -

Can't load Plugin: HogeHoge.dll

                                      • -

Can't load Plugin: PluginSampleDefect.dll

                                      • -

Can't load Plugin: PluginSample.dll

                                      • -

Can't load Plugin: PluginSampleEx.dll

注意

dlcloseした後はプラグインで生成したインスタンスは使えなくなります。
dlopen => Create(new) => delete => dlclose
という順番をしっかり守りましょう。


後はインターフェースクラスに持たせるべき操作を持たせたり、
何かしらのイベントが発生したら、ロード済みの全てのプラグインインスタンスに通知するようにすれば、プラグインによって挙動をかえる事もできそうです。