プラグイン・プログラミング(5)
2006年9月18日………五連続更新、俺的新記録達成である。
STGに喩えるならば最終面である、最終面といえば他の面に比べて長い事が特徴である。
まあ要するに、今回は矢鱈と長くなると思われる。何故ならば、書いたソースが半端じゃなく多いからだ。
★プラグインとは
奇しくも、第一回の講座と同じ段落名である。とはいえ、もうプラグインの説明はしない。
此処での記述事項は、今回作成したプログラムに関する事である。
まず、今回のプログラムは入力した値を返す様に出来ている。
即ち、1を入力→1を表示といった感じだ。超絶シンプルだが、サンプルならシンプルに越した事は無い(Not親父ギャグ)
百聞は一見に如かず、とりあえずはソースコードを以下に記す↓
#include <stdio.h>
int main(){
int Temp;
while(true){
printf("命令を入力してください:");
scanf("%d", &Temp);
switch(Temp){
case(1):
printf("1\n");
break;
case(2):
printf("2\n");
break;
case(3):
printf("3\n");
break;
case(4):
printf("4\n");
break;
case(5):
printf("5\n");
break;
case(6):
printf("6\n");
break;
case(7):
printf("7\n");
break;
case(8):
printf("8\n");
break;
case(9):
printf("9\n");
break;
case(10):
printf("10\n");
break;
default:
return(0);
}
}
}
命令は1〜10の十種類が存在し、それ以外が入力された場合はプログラムを終了する。
物凄くシンプルなプログラムである、これを叩き台にしてプラグインの実装に関して述べていく。
★関数化
まず最初に目立つのはmain関数の長さである、今回はcase後の個々の処理が一行なので短く済んだが実際はそうもいかない。
というワケで、個々の処理部分を独立させて関数化してしまおう。それを行った、ソースコードを以下に記す↓
#include <stdio.h>
void Func1(){
printf("1\n");
}
void Func2(){
printf("2\n");
}
void Func3(){
printf("3\n");
}
void Func4(){
printf("4\n");
}
void Func5(){
printf("5\n");
}
void Func6(){
printf("6\n");
}
void Func7(){
printf("7\n");
}
void Func8(){
printf("8\n");
}
void Func9(){
printf("9\n");
}
void Func10(){
printf("10\n");
}
int main(){
int Temp;
while(true){
printf("命令を入力してください:");
scanf("%d", &Temp);
switch(Temp){
case(1):
Func1();
break;
case(2):
Func2();
break;
case(3):
Func3();
break;
case(4):
Func4();
break;
case(5):
Func5();
break;
case(6):
Func6();
break;
case(7):
Func7();
break;
case(8):
Func8();
break;
case(9):
Func9();
break;
case(10):
Func10();
break;
default:
return(0);
}
}
}
………実際は、短くなるどころか長くなってしまっている(;´Д`)
とはいえ、機能別に分割する事を考慮すると間違ってはいない筈である………多分(;´Д`)
しかしこれでは、1〜10にしか対応不可能である。11を追加したければ、case文を増やす必要がある。
つまり、静的な機能追加のみに対応しており動的な機能追加は不可能である。次に、その点を変更する。
★動的な機能追加
動的に機能を追加するには、map<ユニークID, 関数ポインタ>の様な変数を用意し機能の一覧を保持しておく。
で、追加したい時に変数にinsertすれば良い。以下、それを行ったソースコードを記す↓
#include <stdio.h>
#include <map>
using namespace std;
void Func1(){
printf("1\n");
}
void Func2(){
printf("2\n");
}
void Func3(){
printf("3\n");
}
void Func4(){
printf("4\n");
}
void Func5(){
printf("5\n");
}
void Func6(){
printf("6\n");
}
void Func7(){
printf("7\n");
}
void Func8(){
printf("8\n");
}
void Func9(){
printf("9\n");
}
void Func10(){
printf("10\n");
}
int main(){
int Temp;
map<int, void(*)()> Command;
Command.insert(pair<int, void(*)()>(1, *Func1));
Command.insert(pair<int, void(*)()>(2, *Func2));
Command.insert(pair<int, void(*)()>(3, *Func3));
Command.insert(pair<int, void(*)()>(4, *Func4));
Command.insert(pair<int, void(*)()>(5, *Func5));
Command.insert(pair<int, void(*)()>(6, *Func6));
Command.insert(pair<int, void(*)()>(7, *Func7));
Command.insert(pair<int, void(*)()>(8, *Func8));
Command.insert(pair<int, void(*)()>(9, *Func9));
Command.insert(pair<int, void(*)()>(10, *Func10));
while(true){
printf("命令を入力してください:");
scanf("%d", &Temp);
if(Command.find(Temp)!=Command.end()){
Command.find(Temp)->second();
}
else{
return(0);
}
}
}
main関数が物凄くシンプルに纏まっているのが特徴である、こっちの方が見易いと思われ。
さて、此処まで漸く漕ぎ着けた。やっとDLLの出番である、現在は1〜10にしか対応してないが11〜20にも対応させよう。
★初めてのプラグイン(略してはじぷら)
………何かエロゲーみてぇ、という事は置いといて(´Д`) 以下に、ソースを記す↓
| DLL側(定義ファイル) |
| LIBRARY HOGE EXPORTS Func11 @1 Func12 @2 Func13 @3 Func14 @4 Func15 @5 Func16 @6 Func17 @7 Func18 @8 Func19 @9 Func20 @10 |
DLL側(ソースコード)
#include <stdio.h>
void Func11(){
printf("11\n");
}
void Func12(){
printf("12\n");
}
void Func13(){
printf("13\n");
}
void Func14(){
printf("14\n");
}
void Func15(){
printf("15\n");
}
void Func16(){
printf("16\n");
}
void Func17(){
printf("17\n");
}
void Func18(){
printf("18\n");
}
void Func19(){
printf("19\n");
}
void Func20(){
printf("20\n");
}
EXE側(ソースコード)
#include <stdio.h>
#include <windows.h>
#include <map>
using namespace std;
typedef void (*PFunc)();
void Func1(){
printf("1\n");
}
void Func2(){
printf("2\n");
}
void Func3(){
printf("3\n");
}
void Func4(){
printf("4\n");
}
void Func5(){
printf("5\n");
}
void Func6(){
printf("6\n");
}
void Func7(){
printf("7\n");
}
void Func8(){
printf("8\n");
}
void Func9(){
printf("9\n");
}
void Func10(){
printf("10\n");
}
int main(){
int Temp;
HINSTANCE hLib;
PFunc MyPFunc[10] = {0};
map<int, PFunc> Command;
Command.insert(pair<int, PFunc>(1, *Func1));
Command.insert(pair<int, PFunc>(2, *Func2));
Command.insert(pair<int, PFunc>(3, *Func3));
Command.insert(pair<int, PFunc>(4, *Func4));
Command.insert(pair<int, PFunc>(5, *Func5));
Command.insert(pair<int, PFunc>(6, *Func6));
Command.insert(pair<int, PFunc>(7, *Func7));
Command.insert(pair<int, PFunc>(8, *Func8));
Command.insert(pair<int, PFunc>(9, *Func9));
Command.insert(pair<int, PFunc>(10, *Func10));
hLib = LoadLibrary("HOGE.dll");
if(hLib){
MyPFunc[0] = (PFunc)GetProcAddress(hLib, "Func11");
MyPFunc[1] = (PFunc)GetProcAddress(hLib, "Func12");
MyPFunc[2] = (PFunc)GetProcAddress(hLib, "Func13");
MyPFunc[3] = (PFunc)GetProcAddress(hLib, "Func14");
MyPFunc[4] = (PFunc)GetProcAddress(hLib, "Func15");
MyPFunc[5] = (PFunc)GetProcAddress(hLib, "Func16");
MyPFunc[6] = (PFunc)GetProcAddress(hLib, "Func17");
MyPFunc[7] = (PFunc)GetProcAddress(hLib, "Func18");
MyPFunc[8] = (PFunc)GetProcAddress(hLib, "Func19");
MyPFunc[9] = (PFunc)GetProcAddress(hLib, "Func20");
for(unsigned int i=0;i<10;i++){
if(MyPFunc[i]){
Command.insert(pair<int, PFunc>(i+11, MyPFunc[i]));
}
}
}
while(true){
printf("命令を入力してください:");
scanf("%d", &Temp);
if(Command.find(Temp)!=Command.end()){
Command.find(Temp)->second();
}
else{
FreeLibrary(hLib);
return(getchar());
}
}
}
少しだけ複雑になったが、前回までの講座を読んでいれば理解は出来る筈である。
DLLがEXEと同一のフォルダ内に存在すれば1〜20、存在しなければ1〜10の機能を認識する。
LoadLibraryしてGetProcAddress、それで得たメモリアドレスをCommandにinsertしているだけである。
だが、この実装でプラグインを実現したとは到底言えない。何故ならば、重要な認識が欠けているからだ。
即ち、プラグインはユーザーが作成するもの………DLL側に存在する関数名を、プログラマは知る事が出来ない。
(とはいえ、「関数名を○○にしろ!」とテキストファイルにでも書いておいて中身を自由に実装させるという手段は存在するが)
(※"Hoge.dll"は適切なファイル名に修正すること、ここを修正しないと絶対にLoadLibraryが失敗する(;´Д`))
★進化したプラグイン(略してしんぷら)
………深海プランクトンみてぇ(´Д`)
というのは置いといて、百聞は一見に如かずといった感じでソースを記す↓
| DLL側(定義ファイル) |
| LIBRARY HOGE EXPORTS Func11 @1 Func12 @2 Func13 @3 Func14 @4 Func15 @5 Func16 @6 Func17 @7 Func18 @8 Func19 @9 Func20 @10 |
DLL側(ソースコード)
#include <stdio.h>
#include <windows.h>
typedef void (*PFunc)();
typedef void (*PPFunc)(int, PFunc);
void Func11(){
printf("11\n");
}
void Func12(){
printf("12\n");
}
void Func13(){
printf("13\n");
}
void Func14(){
printf("14\n");
}
void Func15(){
printf("15\n");
}
void Func16(){
printf("16\n");
}
void Func17(){
printf("17\n");
}
void Func18(){
printf("18\n");
}
void Func19(){
printf("19\n");
}
void Func20(){
printf("20\n");
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
if(fdwReason = DLL_PROCESS_ATTACH){
PPFunc MyPFunc;
MyPFunc = (PPFunc)GetProcAddress(GetModuleHandle(NULL), "SetCommand");
if(MyPFunc){
MyPFunc(11, *Func11);
MyPFunc(12, *Func12);
MyPFunc(13, *Func13);
MyPFunc(14, *Func14);
MyPFunc(15, *Func15);
MyPFunc(16, *Func16);
MyPFunc(17, *Func17);
MyPFunc(18, *Func18);
MyPFunc(19, *Func19);
MyPFunc(20, *Func20);
}
}
return(TRUE);
}
EXE側(ソースコード)
#include <stdio.h>
#include <windows.h>
#include <map>
using namespace std;
typedef void (*PFunc)();
map<int, PFunc> Command;
extern "C" __declspec(dllexport) void SetCommand(int ID, PFunc NewFunc){
Command.insert(pair<int, PFunc>(ID, NewFunc));
}
void Func1(){
printf("1\n");
}
void Func2(){
printf("2\n");
}
void Func3(){
printf("3\n");
}
void Func4(){
printf("4\n");
}
void Func5(){
printf("5\n");
}
void Func6(){
printf("6\n");
}
void Func7(){
printf("7\n");
}
void Func8(){
printf("8\n");
}
void Func9(){
printf("9\n");
}
void Func10(){
printf("10\n");
}
int main(){
int Temp;
HINSTANCE hLib;
SetCommand(1, *Func1);
SetCommand(2, *Func2);
SetCommand(3, *Func3);
SetCommand(4, *Func4);
SetCommand(5, *Func5);
SetCommand(6, *Func6);
SetCommand(7, *Func7);
SetCommand(8, *Func8);
SetCommand(9, *Func9);
SetCommand(10, *Func10);
hLib = LoadLibrary("HOGE.dll");
while(true){
printf("命令を入力してください:");
scanf("%d", &Temp);
if(Command.find(Temp)!=Command.end()){
Command.find(Temp)->second();
}
else{
FreeLibrary(hLib);
return(getchar());
}
}
}
(※"Hoge.dll"は適切なファイル名に修正すること、ここを修正しないと絶対にLoadLibraryが失敗する(;´Д`))
漸く此処に至った、今回は流石に初出の事が多いので説明していこう。
・BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
main関数、あるいはWinMain関数に似ていると思ったヤツは正解。DLL版main関数(あるいはWinmain関数)である。
以下、引数に関しての説明を加える。
hinstDLL:DLLインスタンスのハンドルを格納している。
fdwReason:DLLMainが何故呼ばれたか、その理由を格納している。
DLL_PROCESS_ATTACH:プロセス起動時、あるいはLoadLibrary呼出時にこの理由が格納される。つまり、初期化時。
DLL_PROCESS_DETACH:プロセス終了時、あるいはFreeLibrary呼出時にこの理由が格納される。つまり、終了時。
lpvReserved:予約済み引数、即ち触ってはいけない(´Д`)
要するに、DLL初期化時にEXE側に存在する「map<ユニークID, 関数ポインタ>にユニークIDと関数ポインタを登録する関数」を呼び出しているワケだ。
こうする事で、DLL側が自らの関数をEXE側に通知する事が可能となる………DLLはDLLの関数を知っている筈である、EXE側はLoadLibraryするだけでいい。
この実装にした場合、DLL設計者がDLL内の関数にどんな名前をつけようと(返り値、引数等さえ合致していれば)EXE側に存在を通知する事が可能なワケである。
EXE側のソースコード、main関数内に注目。DLLに対するオペレーションは、LoadLibraryとFreeLibrary以外は一切何も行っていない(GetProcAddressが存在しない!)
しかし、実際はこれでも不完全な実装である。何故ならば、この実装ではDLL内の関数名は知らなくて良いがDLLのファイル名は知らなければならないからである。
これでは、多種多様なDLLに対応させる事が実質的に不可能である。では、どうするか………同一フォルダ内のDLLを検索し、LoadLibraryする様な実装が必要である。
★完成したプラグイン(略してかんぷら)
………ガンプラみてぇ(;´Д`) まあ、それは置いといて以下にソースをば↓
| DLL側その1(定義ファイル) |
| LIBRARY HOGE EXPORTS Func1 @1 Func2 @2 Func3 @3 Func4 @4 Func5 @5 Func6 @6 Func7 @7 Func8 @8 Func9 @9 Func10 @10 |
DLL側その1(ソースコード)
#include <stdio.h>
#include <windows.h>
typedef void (*PFunc)();
typedef void (*PPFunc)(int, PFunc);
void Func1(){
printf("1\n");
}
void Func2(){
printf("2\n");
}
void Func3(){
printf("3\n");
}
void Func4(){
printf("4\n");
}
void Func5(){
printf("5\n");
}
void Func6(){
printf("6\n");
}
void Func7(){
printf("7\n");
}
void Func8(){
printf("8\n");
}
void Func9(){
printf("9\n");
}
void Func10(){
printf("10\n");
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
if(fdwReason = DLL_PROCESS_ATTACH){
PPFunc MyPFunc;
MyPFunc = (PPFunc)GetProcAddress(GetModuleHandle(NULL), "SetCommand");
if(MyPFunc){
MyPFunc(1, *Func1);
MyPFunc(2, *Func2);
MyPFunc(3, *Func3);
MyPFunc(4, *Func4);
MyPFunc(5, *Func5);
MyPFunc(6, *Func6);
MyPFunc(7, *Func7);
MyPFunc(8, *Func8);
MyPFunc(9, *Func9);
MyPFunc(10, *Func10);
}
}
return(TRUE);
}
| DLL側その2(定義ファイル) |
| LIBRARY HOGE2 EXPORTS Func11 @1 Func12 @2 Func13 @3 Func14 @4 Func15 @5 Func16 @6 Func17 @7 Func18 @8 Func19 @9 Func20 @10 |
DLL側その2(ソースコード)
#include <stdio.h>
#include <windows.h>
typedef void (*PFunc)();
typedef void (*PPFunc)(int, PFunc);
void Func11(){
printf("11\n");
}
void Func12(){
printf("12\n");
}
void Func13(){
printf("13\n");
}
void Func14(){
printf("14\n");
}
void Func15(){
printf("15\n");
}
void Func16(){
printf("16\n");
}
void Func17(){
printf("17\n");
}
void Func18(){
printf("18\n");
}
void Func19(){
printf("19\n");
}
void Func20(){
printf("20\n");
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
if(fdwReason = DLL_PROCESS_ATTACH){
PPFunc MyPFunc;
MyPFunc = (PPFunc)GetProcAddress(GetModuleHandle(NULL), "SetCommand");
if(MyPFunc){
MyPFunc(11, *Func11);
MyPFunc(12, *Func12);
MyPFunc(13, *Func13);
MyPFunc(14, *Func14);
MyPFunc(15, *Func15);
MyPFunc(16, *Func16);
MyPFunc(17, *Func17);
MyPFunc(18, *Func18);
MyPFunc(19, *Func19);
MyPFunc(20, *Func20);
}
}
return(TRUE);
}
EXE側(ソースコード)
#include <stdio.h>
#include <windows.h>
#include <vector>
#include <map>
using namespace std;
typedef void (*PFunc)();
map<int, PFunc> Command;
vector<HINSTANCE> hLibs;
extern "C" __declspec(dllexport) void SetCommand(int ID, PFunc NewFunc){
Command.insert(pair<int, PFunc>(ID, NewFunc));
}
int main(){
int Temp;
HANDLE hFind;
WIN32_FIND_DATA fd;
hFind = FindFirstFile("*.dll", &fd);
if(hFind!=INVALID_HANDLE_VALUE){
do{
hLibs.push_back(LoadLibrary(fd.cFileName));
if(hLibs.back()==NULL){
hLibs.pop_back();
}
}while(FindNextFile(hFind, &fd));
FindClose(hFind);
}
while(true){
printf("命令を入力してください:");
scanf("%d", &Temp);
if(Command.find(Temp)!=Command.end()){
Command.find(Temp)->second();
}
else{
for(vector<HINSTANCE>::iterator i=hLibs.begin();i!=hLibs.end();i++){
FreeLibrary(*i);
}
return(getchar());
}
}
}
DLL側が二つに増えているが、実際のところ実装は全く変わっていない。よって、今回はEXE側のみを解説する。
まず、最初に注目すべき点はこのEXEには全く機能が含まれていないという事である。要は、何を入力しても即終了。
HOGE.dll(仮)を入れれば1〜10の機能、HOGE2.dll(仮)を入れれば11〜20の機能が使用可能となる(両方なら1〜20)
こうして基本機能すらも外に出してやれば、機能に修正を加えた場合もEXE側のリビルドは不要となる(DLLのみのリビルドでおk)
・ファイル検索
さて、今回の肝はファイル検索である。此処では、各種関数や変数の説明を行う。
・HANDLE hFind;
ファイル検索用ハンドル、ファイル検索に必要な変数である。
・WIN32_FIND_DATA fd;
ファイル検索で該当したファイルの、内部情報を格納している変数である。
・HANDLE FindFirstFile(LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData);
ファイル検索を行い、該当した最初のファイルのハンドルを取得する。
lpFileName:検索ファイル名、ワイルドカードの使用も可能。
lpFindFileData:該当した最初のファイルの内部情報。
・hFind!=INVALID_HANDLE_VALUE
FindFirstFileを実行後、必ずこの様にしてチェックを入れる。
hFind==INVALID_HANDLE_VALUEだった場合、ファイル検索は失敗している(存在しない、あるいは何らかの要因で)
・BOOL FindNextFile(HANDLE hFind, LPWIN32_FIND_DATA FindData);
ファイル検索を行い、次のファイルのハンドルを取得する。
つまり、FindFirstFileを実行後はFindNextFileを呼び出し続ければ良い。
返り値で成否を確認可能、即ちfalseが返ってきたらそれ以上は存在しないと解釈して構わない。
・do〜while
hLibsに、fd.cFileNameでLoadLibraryして得られたHINSTANCEをpush_backする。
その後にNULLチェックを行い、NULL(=LoadLibraryに失敗)ならpop_backする。
・BOOL FindClose(hFind);
ファイル検索用ハンドルを解放、即ちファイル検索を終了する。
注意点は、失敗したファイル検索用ハンドルやNULLを引数にしない事。
下手すると、スレッド死亡なんていうマジで洒落にならん事態になりかねない。
後は、終了時にイテレータで巡回して順次FreeLibraryを行っている位である。
これでプラグインの実装法に関する講座は終了である、今回クラスは全く使わなかったがクラスを使った場合も似た様なものである。
………以上、これで(5)は終了である。以上でプラグイン・プログラミングは終了、見ていたヤツはお疲れさん。
初めにも記したが勉強しながらの講座が故に、間違っている部分が存在しているかもしれない。その場合、拍手なりメールなりで指摘してもらえれば筆者としては僥倖である。