プラグイン・プログラミング(4)
2006年9月17日………四連続更新である、俺って凄いね♪(何処が)
さて、今回はDLLでクラスを取り扱ってみる。本来DLLはクラス非対応なのだが、対応させる事が出来るのだ。
★何故DLLはクラス非対応なのか?
端的に言うと、GetProcAddressに相当するクラス取得関数が存在しないからである。
また、エクスポートテーブルはあくまでもエクスポートされた「関数」の一覧表でしかない。
よって、DLL内に存在するクラスを利用するには「外部」に何とかして公開する必要がある。
★HOW?
そもそも、EXE「外部」からクラスを取り込むには二種類の方法が存在する。
今回のお題はあくまで「DLLでクラスを取り扱う」なのだが、一応紹介だけはしておこう。
・COM
プログラマならば名前位は聞いた事がある筈だ、つ〜か俺も名前位しか知らん(´Д`)
知っている限りでは、(1)バージョン管理が可能(2)クラス対応(3)レジストリに登録される………といった感じである。
(1)はDirectXが代表例である、現在のDirectXは9.0cだが未だにDirectX7.0を用いたゲームがきちんと動作する事に注目してほしい。
(2)は、COMでクラスが扱えるという事を指し示している。だが待ってほしい、問題は(3)のレジストリに登録されるという事に存在する。
つまり、プラグインでCOMを用いると追加される度にレジストリが侵食されていく。これは流石にまずい、ってか少なくとも俺は使いたくない。
よって、COMは回避の方向となる(新しくCOMの勉強をするのも面倒だしな)
・DLL
何度も繰り返すようだが、今回はDLLでクラスを取り扱う方法を紹介する。では、以下でどの様にすれば良いのか順を追って書いていこう。
まず、クラスは関数と変数で構成されている。変数はクラス内部で働き、関数はクラス外部との連絡を行う(変数をpublicにするのは非推奨)
つまり、変数は気にする必要がない。欲しいのは関数である、つ〜か変数が欲しかったらGetter/Setterをきちんと作る事(C++の基本である)
よって、関数を全て渡せばクラスを取り扱える。では、どうやって関数のメモリアドレスを渡すのか?………答えは、仮想関数テーブルにある。
★仮想関数テーブル
仮想関数テーブルとは、C++の重要なパラダイムのひとつである継承を実現する為の仕組みである。
仮想関数は必ず仮想関数テーブルを持つ、仮想関数テーブルには関数名と対応するメモリアドレスが記されている。
で、関数が呼び出された時にどの関数を呼べば良いのか仮想関数テーブルを参照して決定するワケである。以下、図で説明してみる↓
クラス | 仮想関数テーブル |
class TestP{ public: virtual ~TestP(){} void FuncA(){} virtual void FuncB(){} }; ・FuncAは仮想関数テーブルを持っていない |
FuncB:TestP::FuncBのメモリアドレス |
class TestC : public TestP{ public: void FuncA(){} void FuncB(){} }; ・FuncAは仮想関数テーブルを持っていない |
FuncB:TestC::FuncBのメモリアドレス |
つまり、仮想関数は呼び出される→関数名からメモリアドレスを参照→メモリアドレスをアクセスといった手順を行っている。
結論:関数を全て渡すには、ひとつひとつエクスポートするよりも全ての関数を仮想関数にして仮想関数テーブルを渡してしまう方が良い。
★具体的な実装
では、具体的な実装の話に入ろう。まずは、幾つかの注意点から述べていく。
・全ての関数を仮想関数にする
これは大前提である、しなければ仮想関数テーブルに登録されないので呼び出せない。
・クラスは必ず単一継承にする
仮想関数テーブルはひとつでなければならない、多重継承すると複数の仮想関数テーブルが出来てしまう。
・クラスに対応したFuctory関数を用意する
Fuctory関数とは、オブジェクトを生成する関数の事である。少し分かりにくいので、具体的なソースコードを以下に示す↓
TestP* FuctoryFunc(){ return(new TestC); }
上記の関数をEXE側から呼び出し、DLL側から生成されたクラスを受け取って使用するワケだ。
・クラスに対応したDeleteInstance関数を用意する
コンパイラは複数存在する、たとえばVisualC++(VC)とBorlandC++Compiler(BCC)のように。
そして、標準関数の内部処理がそれらで同一であるという保証は存在しない。即ち、VCとBCCのprintfは異なるものだ。
それはnew/deleteにも該当する、故に決してVCでnewしたオブジェクトをBCCでdeleteしたり、その逆を行ってはならない。
よって、DLL側で生成したオブジェクトはDLL側で削除する。方法としては、以下の二種類が存在する↓
DeleteInstance関数 | Suicide関数 |
void DeleteInstance(TestP* Obj){ delete(Obj); } |
void TestC::Suicide(){ delete(this); } |
………要は、関数として用意するかメンバ関数として用意するかの違いである。個人的には、メンバ関数での用意を推奨しておく。
★サンプル
では、具体的なソースコードを以下に記そう↓
(※以下のソースコードには間違いが存在する、詳細はプラグイン・プログラミング(8)で)
DLL側(定義ファイル) |
LIBRARY HOGE EXPORTS CreateInstance @1 |
DLL側(ソースコード)
#include <stdio.h> #include <windows.h> class TestP{ public: virtual ~TestP(){} virtual void Suicide() = 0; virtual void TestFunc() = 0; }; class TestC : public TestP{ public: TestC(){ printf("こんすとらくた(TestC);\n"); } ~TestC(){ printf("ですとらくた(TestC);\n"); } void Suicide(){ delete(this); } void TestFunc(){ printf("てすとふぁんく(TestC);\n"); } }; extern "C"{ TestP* CreateInstance(){ return(new TestC); } }
EXE側(ソースコード)
#include <stdio.h> #include <windows.h> class TestP{ public: virtual ~TestP(){} virtual void Suicide() = 0; virtual void TestFunc() = 0; }; typedef TestP* (*PFunc)(); int main(){ HINSTANCE hLib; TestP* Temp = NULL; PFunc MyFunc = NULL; hLib = LoadLibrary("HOGE.dll"); if(hLib){ MyFunc = (PFunc)GetProcAddress(hLib, "CreateInstance"); } else{ printf("HOGE.dllの取得に失敗;\n"); } if(MyFunc){ Temp = MyFunc(); } else{ printf("CreateInstanceの取得に失敗;\n"); } if(Temp){ Temp->TestFunc(); Temp->Suicide(); } FreeLibrary(hLib); return(getchar()); }
注目すべき点は、DLLのソースコードとEXEのソースコードの両方にTestPが含まれているという事である。
こうしてDLLとEXEの両方にインターフェースクラスを用意し、DLL側で継承させて派生クラスを作ってもらう。
以上、この様にする事でDLLにおいてクラスが扱える様になる(派生クラスしか扱えないが、普通はそれで十分な筈だ)
(※"Hoge.dll"は適切なファイル名に修正すること、ここを修正しないと絶対にLoadLibraryが失敗する(;´Д`))
………以上、これで(4)は終了である。次回、プラグインの実装方法について語る予定。
クラスに関しては意外に早く終われた感じだ、次回やるべき事も見えてはいるので近日中に更新出来るだろう。………た、多分(;´Д`)