プラグイン・プログラミング(2)

 

2006年9月15日………実は、前回の講座を書き終えてから数時間後にこれを書いている。
前編は抽象的な説明に終始したが、今編は具体的なソースコードを例示してどのようにプラグインを実装すれば良いかを示してみる。
とはいえ、俺もDLLやプラグインに関してはずぶの素人である。勉強しつつ書いているので間違いも多々あるかもしれないが、責任は取れない(´Д`)
また、前編では説明しきれなかったプラグインを実装する上で理解する必要がある概念も多々存在する。そういうものは、出てきた時にでも説明していく予定である。

★DLLの作成

さっそく、ソースコードを見ていこう。

#include <stdio.h>
#include <windows.h>

extern "C" __declspec(dllexport) void __stdcall PDll(){
	printf("PDll;\n");
}

extern "C"、__declspec(dllexport)、__stdcall………C&C++を知っている人も、あまり見た事が無いような表記が多い。
これ等を説明するには、まずDLLを作成する上で理解する必要がある概念を説明していかなければならない。

・エクスポートテーブル

        まずは、エクスポートテーブルについて説明していこうと思う。
        前編でも軽く触れたが、プログラムとは本来それ自身(実行ファイル、.exeってファイルね)で完結している。
        つまり、DLLの様な「EXEさんEXEさん、この関数利用して下さいな(´Д`)」といった在り様は標準的ではない。
        要するに関数が「外部」に公開されておらず、「外部」からは参照できない状態である。そもそも、関数名は人間の為のものである。
        よって、関数名はコンパイル・リンク時にコンピューターが分かり易い別の形に変換される(多分、メモリアドレスあたりに変換される)
        そして、一度変換されたら外部からは参照不可である。何故なら、そんな名前は既にコンパイル・リンク時に破棄され存在しないからだ。
        そこでエクスポートテーブルである、これは関数名とメモリアドレスを一対一で関連付けた表を連想してくれるのが一番実体に近いと思う。
        これをDLLは保持し、EXEから関数呼び出しを受けた場合に表を比較し該当するメモリアドレスを指し示す。こうする事で、関数を名前で呼び出せる。

・装飾名

        上記でエクスポートテーブルを、関数名とメモリアドレスを一対一で関連付けた表と説明した。
        実はそれ、真っ赤とは言わずとも橙色位の嘘である。実際は、関数名ではなく「装飾名」というのを用いる。
        何故そんな面倒臭い事をするのか、理由は言語仕様にある。C++は、同一の関数名でも引数が違えば別の関数として存在する事を許容している。
        よって、関数名ではメモリアドレスを特定不可能になってしまう。何故なら、SetParam(int Param)もSetParam(string Param)も同じ「関数名」だからだ。
        それに加えて、名前空間やクラス等の概念によっても同一関数名は許容される。よって、関数名でエクスポートテーブルを作るのは事実上不可能である。
        そこで装飾名の出番である、装飾名はコンパイラが決定したエクスポートテーブルの「関数名」である。装飾名は名前空間やクラス、引数等で決められる。
        具体的に、VC++7での装飾名を見てみよう↓

関数名 void h(int) void h(int, char) void h(void)
装飾名 ?h@@YAXH@Z ?h@@YAXHD@Z ?h@@YAXXZ

        ………「こんなん覚えてられっか、ふざけんな!ヽ(`Д´)ノ」と言われても仕方が無い気がする、つ〜か俺はそうと言いたい叫びたい。
        実は、装飾名を非常に簡単にする方法があるのだ………詳細は、下記にて記す事とする。一応、此処よりも詳しい文献にリンクしておく(

・呼び出し規約

        関数の呼び出し方の事である、ってまんまやな(´Д`) 要は、呼び出し方はひとつではないという事だ。
        たとえば、void SetXY(int x, int y);という関数ひとつ取ってみても………

        ・x→yの順番でスタックに積むのか、y→xの順番でスタックに積むのか(仮引数のスタックへの積み方)

        ・仮引数を、どこで処分するのか(呼んだ側で仮引数を処分するのか、呼ばれた側で仮引数を処分するのか)

        2×2=4通りの処理方法(呼び出し方)が存在する事が分かるだろうか? これを、呼び出し規約という。
        たとえばCやC++はy→xで呼んだ側、Pascal(Delphi)はx→yで呼ばれた側といった様に言語仕様によって標準が定められている。
        しかし、特定の関数だけを別の呼び出し規約に変更する事は可能である。これは恐らく、別言語からの関数呼び出し等を考慮しての事だろう。
        我ながら物凄くアバウトな説明だが、詳細は此処よりも詳しい文献で見てほしい(

以上、アバウトな説明終了。では、具体的な説明に入る(´Д`)

・extern "C"

        装飾名の命名規則を、C++の命名規則からCの命名規則に変える為のもの。
        装飾名の一番の理想は関数名そのままだが、そうはいかない。しかし、上記の様な装飾名(?h@@YAXH@Z)を覚え使用するのは真っ平御免である。
        そこでextern "C"の出番である、C++は名前空間やクラスの存在が故に命名規則が複雑だがCは名前空間やクラスが存在しない為に命名規則が比較的単純である。
        具体的に、VC++7でのextern "C"した場合の装飾名を見てみよう↓

関数名 void h(int) void h(int, char) void h(void)
装飾名(cdecl) _h _h _h
装飾名(stdcall) _h@4 _h@5 _h@0
装飾名(fastcall) @h@4 @h@5 @h@0

        ………これなら何とか覚えられそうではある、ちなみに数字は(聡いヤツなら気付いたかもしれんが)引数のサイズ(単位:バイト)と同じである(int=4バイト、int+char=5バイト)
        ちなみに、Microsoft製のコンパイラ(VisualC++とかね)では後述する__declspec(dllexport)で修飾した場合に先頭の「_」が削除されるようだ(ソース
        cdeclやstdcall、fastcallというのは下記にて簡潔に説明する予定である。

・__declspec(dllexport)

        これで修飾された関数はエクスポートテーブルに登録される、以上

・__stdcall

        呼び出し規約のひとつ、CやC++における呼び出し規約は(俺の知る限りでは)三つである。

        cdecl:CやC++における標準 、可変引数をとる関数はこの呼び出し規約でしか宣言出来ない。

        stdcall:WindowsAPIにおける標準、即ちこれを用いて宣言した関数は他の言語でも使用可能(WindowsAPIが使用可能な言語ならだが)

        fastcall:引数をスタックではなくレジスタに格納する、即ち他の呼び出し規約よりは「早い」(とはいえ、昨今のPC性能ならわざわざ使うものではない)

        よって通常はcdeclを用い、他の言語から参照される可能性がある場合はstdcallを用いるのが乙女プログラマの嗜みである(cdeclは明記不要だが)

★DLLのインポート

#include <stdio.h>
#include <windows.h>

typedef void (__stdcall *PMFunc)();

int main(){
	HINSTANCE hLib;
	PMFunc MyPMFunc = NULL;

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		MyPMFunc = (PMFunc)GetProcAddress(hLib, "_PDll@0");
	}
	else{
		printf("Hoge.dllの取得に失敗;\n");
	}

	if(MyPMFunc){
		MyPMFunc();
	}
	else{
		printf("_PDll@0の取得に失敗;\n");
	}

	FreeLibrary(hLib);

	return(getchar());
}

・typedef void (__stdcall *PMFunc)();

        関数ポインタの定義、DLLに入っている関数のメモリアドレスを保存する為に使用する。

・hLib = LoadLibrary("Hoge.dll");

        DLLを呼び出し、EXEとの動的結合を行う。失敗時は、NULLが返る。

・MyPMFunc = (PMFunc)GetProcAddress(hLib, "_PDll@0");

        DLLに入っている関数のメモリアドレスを取得し、PMFuncにキャストしてMyPMFuncに代入している。
        一番目の引数にはLoadLibraryで取得したインスタンスを、二番目の引数には関数の装飾名を渡す。
        失敗時はLoadLibraryと同じく、NULLが返る。

・FreeLibrary(hLib);

        EXEとの動的結合を解除し、DLLを解放する。解放しないと、メモリに残りっ放しになるので注意!(メモリリーク以上に性質が悪く、バグの原因になりがち)

以上で、DLLの作成/DLLのインポートは終了である。早速、試してみるべし(*-*b
(※"Hoge.dll"は適切なファイル名に修正すること、ここを修正しないと絶対にLoadLibraryが失敗する(;´Д`))

★DLLの作成(2)

DLLの作成、及びDLLのインポートは上記にて説明をひとまず終えた。だが、作成及びインポートの方法は必ずしもひとつではない。
上記の場合の装飾名は非常に単純なものだが、それでも面倒臭いといえば面倒臭い。つ〜か、可変引数の場合はどうするんですかおやっさん(´Д`)

………というワケなので、もうひとつの方法を紹介する。こっちの方がより簡単な装飾名で扱える、どちらかというとオススメの方法である(なら先に出せよ)

#include <stdio.h>
#include <windows.h>

extern "C" void PDialog(HWND MyApp, LPCSTR fmt, ... );

extern "C" void PDialog(HWND MyApp, LPCSTR fmt, ... ){
	char buf[1025];
	const int n = wvsprintf(buf, fmt, (va_list)(&fmt+1)) - 1;
	if(buf[n]=='\n'){
		buf[n+1] = '\0';
	}
	MessageBox(MyApp, buf, "だいあろ〜ぐ(´Д`)", MB_OK | MB_ICONWARNING);
}

………前回と異なるのは、(まあ可変引数な関数だとかそういう部分もあるが)__declspec(dllexport)と__stdcallが無い事である。
で、今回は以下の内容が必要となる↓

LIBRARY HOGE
EXPORTS
    PDialog @1

上記の内容は、定義ファイルに書く。

・定義ファイル

        さて、また新概念の登場ですな('='
        定義ファイルとは、いわばエクスポートテーブルそのものである。
        ファイルとしては拡張子が.defのテキストファイルに過ぎないが、関数名を書く事でエクスポートテーブルに登録できる。
        VC++7の場合、新しい項目の追加→モジュール定義ファイルで追加可能。普通に、(エクスプローラから)新規作成→リネーム→既存項目追加でも構わない。
        一応、簡単な説明を行っておく↓

                LIBRARY:ライブラリ名を書く、上記の場合はHOGEというライブラリとなる

                EXPORTS:関数名と序数を書く、序数は自由に設定可能だが決して重複してはならない(ユニークIDみたいなもの)

        とりあえず、詳細は此処よりも詳しい文献で見てほしい(

………説明終了、要はこの方法だと可変引数や大規模なクラスや構造体が引数の場合もお手軽に済ませる事が可能である。

・__declspec(dllexport)がない

        モジュール定義ファイルがあるんだから要らん(´Д`)

・__stdcallがない

        可変引数はcdeclにしなければならない、理由としてはいくつ引数が渡されるか分からないから。
        よって、可変引数の関数は他の言語から呼び出せる保証が存在しない(つ〜か、呼び出しちゃ駄目(´Д`))

★DLLのインポート(2)

引き続き、DLLのインポート↓

#include <stdio.h>
#include <windows.h>

typedef void (*PMFunc)(HWND MyApp, LPCSTR fmt, ... );

int main(){
	HINSTANCE hLib;
	PMFunc MyPMFunc = NULL;

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		MyPMFunc = (PMFunc)GetProcAddress(hLib, "PDialog");
	}
	else{
		printf("Hoge.dllの取得に失敗;\n");
	}

	if(MyPMFunc){
		MyPMFunc(NULL, "てすと");
	}
	else{
		printf("PDialogの取得に失敗;\n");
	}

	FreeLibrary(hLib);

	return(getchar());
}

GetProcAddressの引数で、装飾名ではなく関数名(正確には、関数名と同一の装飾名)を使えている事に注目。
また、(PMFunc)GetProcAddress(hLib, "PDialog");は(PMFunc)GetProcAddress(hLib, (LPCSTR)1);と書く事も可能(序数で関数のメモリアドレスを取得できる)
(※"Hoge.dll"は適切なファイル名に修正すること、ここを修正しないと絶対にLoadLibraryが失敗する(;´Д`))

 

 

 

………以上、これで(2)は終了である。今回で、DLLの作成/DLLのインポートは完璧な筈だ。次回は、DLLから本体側の関数を呼び出すテクニックについて書く予定('='

 

前へ                                        戻る                                        次へ