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

 

2007年1月26日………日記の更新代わりに、プログラミング講座をこそこそと進行中(・∀・)
気付いている人ってどんくらい居るのだろうか、つ〜か気付くも何もそもそも見てな(略

というワケで、ここではプラグイン・プログラミング(7)のサンプルコードを掲載しよう。

★POD型構造体(+パディング)

DLL側(定義ファイル)
EXPORTS
    _SetTest = SetTest

DLL側(ソースコード)

#include <string.h>
#include <windows.h>

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void SetTest(Test* Obj){
	Obj->nMem = 20;
	strcpy(Obj->strMem, "Dll");

	PFunc fpPrintTest = (PFunc)GetProcAddress(GetModuleHandle(NULL), "_PrintTest");
	fpPrintTest(Obj);
}

EXE側(ソースコード)

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void PrintTest(Test* Obj){
	printf("Test::nMem = %d\n", Obj->nMem);
	printf("Test::strMem = %s\n", Obj->strMem);
}

int main(){
	Test Obj;
	HMODULE hLib;
	PFunc fpSetTest = NULL;

	Obj.nMem = 10;
	strcpy(Obj.strMem, "Main");
	printf("Test::nMem = %d\n", Obj.nMem);
	printf("Test::strMem = %s\n", Obj.strMem);

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		fpSetTest = (PFunc)GetProcAddress(hLib, "_SetTest");
	}
	else{
		printf("Hoge.dllの取得に失敗!\n");
	}

	if(fpSetTest){
		fpSetTest(&Obj);
	}
	else{
		printf("_SetTestの取得に失敗!\n");
	}

	FreeLibrary(hLib);
	return(getchar());
}

………「#pragma pack(4)」を追加しただけである、要らないかもね(;´Д`)

★POD型+α構造体

DLL側(定義ファイル)
EXPORTS
    _SetTest = SetTest

DLL側(ソースコード)

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
	void Clear(){
		nMem = 0;
		strcpy(strMem, "");
	}
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void SetTest(Test* Obj){
	PFunc fpPrintTest = (PFunc)GetProcAddress(GetModuleHandle(NULL), "_PrintTest");

	printf("Begin:SetTest\n");

	printf("sizeof(Test) = %d\n", sizeof(Test));

	Obj->Clear();
	fpPrintTest(Obj);

	Obj->nMem = 20;
	strcpy(Obj->strMem, "Dll");
	fpPrintTest(Obj);

	printf("End:SetTest\n");
}

EXE側(ソースコード)

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
	void Clear(){
		nMem = 0;
		strcpy(strMem, "");
	}
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void PrintTest(Test* Obj){
	printf("Test::nMem = %d\n", Obj->nMem);
	printf("Test::strMem = %s\n", Obj->strMem);
}

int main(){
	Test Obj;
	HMODULE hLib;
	PFunc fpSetTest = NULL;

	printf("Begin:Main\n");

	printf("sizeof(Test) = %d\n", sizeof(Test));

	Obj.Clear();
	PrintTest(&Obj);

	Obj.nMem = 10;
	strcpy(Obj.strMem, "Main");
	PrintTest(&Obj);

	printf("End:Main\n");

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		fpSetTest = (PFunc)GetProcAddress(hLib, "_SetTest");
	}
	else{
		printf("Hoge.dllの取得に失敗!\n");
	}

	if(fpSetTest){
		fpSetTest(&Obj);
	}
	else{
		printf("_SetTestの取得に失敗!\n");
	}

	FreeLibrary(hLib);
	return(getchar());
}

プラグイン・プログラミング(7)をきちんと読んだ人は分かるだろうが、void Test::Clear()をnMemとstrMemの間や一番上に移動させても全く問題は生じなかった。
これは、void Test::Clear()がTestから完全に切り離されている証と言えるだろう。とはいえ、普通はいちいちメンバ関数の位置とか変えるものではないが(´Д`)

★POD型+α構造体(実験:void Test::Clear()の挙動をDLLとEXEで変えてみる)

DLL側(定義ファイル)
EXPORTS
    _SetTest = SetTest

DLL側(ソースコード)

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
	void Clear(){
		nMem = 0;
		strcpy(strMem, "ClearedInDLL");
	}
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void SetTest(Test* Obj){
	PFunc fpPrintTest = (PFunc)GetProcAddress(GetModuleHandle(NULL), "_PrintTest");

	printf("Begin:SetTest\n");

	printf("sizeof(Test) = %d\n", sizeof(Test));

	Obj->Clear();
	fpPrintTest(Obj);

	Obj->nMem = 20;
	strcpy(Obj->strMem, "Dll");
	fpPrintTest(Obj);

	printf("End:SetTest\n");
}

EXE側(定義ファイル)
EXPORTS
    _PrintTest = PrintTest

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
	void Clear(){
		nMem = 0;
		strcpy(strMem, "ClearedInEXE");
	}
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void PrintTest(Test* Obj){
	printf("Test::nMem = %d\n", Obj->nMem);
	printf("Test::strMem = %s\n", Obj->strMem);
}

int main(){
	Test Obj;
	HMODULE hLib;
	PFunc fpSetTest = NULL;

	printf("Begin:Main\n");

	printf("sizeof(Test) = %d\n", sizeof(Test));

	Obj.Clear();
	PrintTest(&Obj);

	Obj.nMem = 10;
	strcpy(Obj.strMem, "Main");
	PrintTest(&Obj);

	printf("End:Main\n");

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		fpSetTest = (PFunc)GetProcAddress(hLib, "_SetTest");
	}
	else{
		printf("Hoge.dllの取得に失敗!\n");
	}

	if(fpSetTest){
		fpSetTest(&Obj);
	}
	else{
		printf("_SetTestの取得に失敗!\n");
	}

	FreeLibrary(hLib);
	return(getchar());
}

なんと、まともに動きやがります(゜Д゜) まあ、論理的に考えても動くわな(;´Д`)
でも、こういう事をしてると「あれ?この関数ってDLLとEXEで挙動違うっけ?」とか考えて混乱したりするかもしれない。
つ〜か、絶対天罰が下ったりバグの原因になったりするね。つ〜わけで、良い子の皆さんは絶対に使わないように(;´Д`)

★new/deleteを普通にDLLとEXEの間で使ってみる(1)(※バグ注意)

DLL側(定義ファイル)
EXPORTS
    _DeleteTestInDLL = DeleteTestInDLL

DLL側(ソースコード)

#include <windows.h>

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
};

typedef void (*PFunc)(Test*);

extern "C" __declspec(dllexport) void DeleteTestInDLL(Test* Obj){
	delete(Obj);
}

EXE側(ソースコード)

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
};

typedef void (*PFunc)(Test*);

int main(){
	HMODULE hLib;
	PFunc fpDeleteTestInDLL = NULL;

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		fpDeleteTestInDLL = (PFunc)GetProcAddress(hLib, "_DeleteTestInDLL");
	}
	else{
		printf("Hoge.dllの取得に失敗!\n");
	}

	if(!fpDeleteTestInDLL){
		printf("_DeleteTestInDLLの取得に失敗!\n");
	}
	else{
		Test* Obj = new Test;
		fpDeleteTestInDLL(Obj);
	}

	FreeLibrary(hLib);
	return(getchar());
}

(※バグ注意)とあるように、上記のソースコードはバグを内在している。
即ち、プラグイン・プログラミング(7)で扱ったnewとdeleteの問題である。まずは、EXEで確保→DLLで解放のテストである。
VCの場合はDebugとReleaseで生成されるバイナリコードが異なる(デバッグ用のassert等が組み込まれる)ので、様々なチェックをせねばなるまい。

DLL\EXE BCC(Debug) BCC(Release) VC(Debug) VC(Release)
BCC(Debug) エラー表示有り エラー表示有り エラー表示有り エラー表示有り
BCC(Release) エラー表示有り エラー表示有り エラー表示有り エラー表示有り
VC(Debug) エラー表示有り エラー表示有り エラー表示有り エラー表示有り
VC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し

俺が驚いたのは、同種のコンパイラで同じランタイムを使用しているのにアクセス違反が発生するということにである。
具体的にはBCC(Debug)×BCC(Debug)、BCC(Release)×BCC(Release)、VC(Debug)×VC(Debug)の三つである。
また、VC(Release)のDLLだとどの組み合わせでもアクセス違反が発生しない(少なくともエラーが表示されない)のにも驚いた。
つ〜か、BCC(Debug)×BCC(Debug)、BCC(Release)×BCC(Release)、VC(Debug)×VC(Debug)でエラーが起こるって………(;´Д`)
まあ、百歩譲ってBCC(Debug)×BCC(Debug)とVC(Debug)×VC(Debug)は許そう………だが、BCC(Release)×BCC(Release)だけは許せない。
前者はDebugなので厳密にチェックして弾いているとも考えられるが、後者はなぁ………BCCで、new/deleteをするようなDLLを作るなと言わんばかりである。
まあ、元々new/deleteするのが裏技であり反則技なワケだが………ああ、ひとつ注意しておくとVC(Release)×VC(Release)が完全に安全だとは誰も言っていない。
あくまで「エラー表示無し」なのである、メモリリークが起きているかまでは流石に分からない………つ〜か、誰か外部からメモリを調べるツールを俺に教えてくれ(;´Д`)

あるいは、「new/deleteをオーバーロードせずに使用するの禁止!(#`ω´)」とBCC(Release)は言ってるのかもしれん………もしもそうなら、あるいはプログラマには親切かも。

★new/deleteを普通にDLLとEXEの間で使ってみる(2)(※バグ注意)

DLL側(定義ファイル)
EXPORTS
    _CreateTestInDLL = CreateTestInDLL

DLL側(ソースコード)

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
};

extern "C" __declspec(dllexport) Test* CreateTestInDLL(){
	return(new Test);
}

EXE側(ソースコード)

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

#pragma pack(4)

struct Test{
	int nMem;
	char strMem[20];
};

typedef Test* (*PFunc)();

int main(){
	HMODULE hLib;
	PFunc fpCreateTestInDLL = NULL;

	hLib = LoadLibrary("Hoge.dll");

	if(hLib){
		fpCreateTestInDLL = (PFunc)GetProcAddress(hLib, "_CreateTestInDLL");
	}
	else{
		printf("Hoge.dllの取得に失敗!\n");
	}

	if(!fpCreateTestInDLL){
		printf("_CreateTestInDLLの取得に失敗!\n");
	}
	else{
		Test* Obj = fpCreateTestInDLL();
		delete(Obj);
	}

	FreeLibrary(hLib);
	return(getchar());
}

(※バグ注意)とあるように、上記のソースコードはバグを内在している。
即ち、プラグイン・プログラミング(7)で扱ったnewとdeleteの問題である。次は、DLLで確保→EXEで解放のテストである。

DLL\EXE BCC(Debug) BCC(Release) VC(Debug) VC(Release)
BCC(Debug) エラー表示無し エラー表示無し エラー表示有り エラー表示無し
BCC(Release) エラー表示無し エラー表示無し エラー表示有り エラー表示無し
VC(Debug) エラー表示無し エラー表示無し エラー表示有り エラー表示無し
VC(Release) エラー表示有り エラー表示有り エラー表示有り エラー表示無し

………ううむ、何だかよく分からない結果だ(;´Д`)
分かるのはEXEがVC(Debug)だとエラーが表示され、DLLがVC(Release)だとEXEがVC(Release)な場合を除いてエラーが表示されるということである。
まあ、VC(Release)×VC(Release)がエラーにならないのは理解出来るしEXEがVC(Debug)だとエラーになるのも理解できる(delete時にチェックしているのだろう)
だがしかし、VC(Release)がDLLでBCC(Debug)あるいはBCC(Release)がEXEだとエラーが表示されるというのは………いや、まあ異種コンパイラなのだから理解は出来るのだが。
上のEXEで確保→DLLで解放とは全く異なる結果だが、こういうのを不定という………即ち、動作が予測不可能で安定性が見られない。こんなプログラム、流石にゲームには使えまい。

★EXE側のnew/deleteをDLL側が呼び出す

DLL側(ソースコード)

#include <windows.h>

typedef void* (*NewFunc)(size_t);
typedef void (*DeleteFunc)(void*);

NewFunc fpNew = NULL;
DeleteFunc fpDelete = NULL;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
	if(fdwReason = DLL_PROCESS_ATTACH){
		fpNew = (NewFunc)GetProcAddress(GetModuleHandle(NULL), "_New");
		fpDelete = (DeleteFunc)GetProcAddress(GetModuleHandle(NULL), "_Delete");
	}
	return(true);
}		

void* operator new(size_t t){
	return fpNew(t);
}
void operator delete(void* p){
	fpDelete(p);
}

void* operator new[](size_t t){
	return fpNew(t);
}
void operator delete[](void* p){
	fpDelete(p);
}

EXE側(定義ファイル)
EXPORTS
    _New = New
    _Delete = Delete

EXE側(ソースコード)

extern "C" __declspec(dllexport) void* New(size_t t){
	return new BYTE[t];
}
extern "C" __declspec(dllexport) void Delete(void* p){
	delete[] p;
}

同じコードを何度も掲載するのも無駄なので、追加部分のみを上記に示す。
要は、上記のソースコードを該当するソースコードの先頭で追加してやればよい。

EXEで確保→DLLで解放

DLL\EXE BCC(Debug) BCC(Release) VC(Debug) VC(Release)
BCC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
BCC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し

DLLで確保→EXEで解放

DLL\EXE BCC(Debug) BCC(Release) VC(Debug) VC(Release)
BCC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
BCC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し

プラグイン・プログラミング(7)で述べたとおり、全ての組み合わせでエラーが表示されなくなった。
まあ考えてみると当然だろう、完全に同一なnew/deleteを使っているのにエラーが発生する筈が無い。

★必ず共通の処理となるnew/deleteを行う

DLL側(ソースコード)

#include <windows.h>

void* operator new(size_t size){
	return(HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE | HEAP_GENERATE_EXCEPTIONS, size));
}
void* operator new[](size_t size){
	return(HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE | HEAP_GENERATE_EXCEPTIONS, size));
}
void operator delete(void* ptr){
	HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, ptr);
}
void operator delete[](void* ptr){
	HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, ptr);
}

EXE側(ソースコード)

void* operator new(size_t size){
	return(HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE | HEAP_GENERATE_EXCEPTIONS, size));
}
void* operator new[](size_t size){
	return(HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE | HEAP_GENERATE_EXCEPTIONS, size));
}
void operator delete(void* ptr){
	HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, ptr);
}
void operator delete[](void* ptr){
	HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, ptr);
}

こちらも上と同じく追加部分のみを上記に記す、該当するソースコードの先頭に追加すること。

EXEで確保→DLLで解放

DLL\EXE BCC(Debug) BCC(Release) VC(Debug) VC(Release)
BCC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
BCC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し

DLLで確保→EXEで解放

DLL\EXE BCC(Debug) BCC(Release) VC(Debug) VC(Release)
BCC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
BCC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Debug) エラー表示無し エラー表示無し エラー表示無し エラー表示無し
VC(Release) エラー表示無し エラー表示無し エラー表示無し エラー表示無し

こちらも上と同じく、全ての組み合わせでエラーが表示されなくなった。
まあ、表示されていないだけで実際にはメモリリークが生じている可能性もあるのだが………杞憂だろう、多分(;´Д`)
その辺は、外部からメモリの消費量をモニタしてくれるツールがあれば分かるのだが………残念ながら、俺はそういったものを持ってはいない。

 

 

 

以上、これで(7.5)は終了である。

 

戻る