プラグイン・プログラミング(7)
2007年1月21日………実にどうでもいいことなのだが、今日は俺の誕生日だぜヽ(・∀・)ノ●ウンコー
………何故に「ヽ(・∀・)ノ●ウンコー」AAを多用しているのだろう、それは(俺自身にすら)謎である(;´Д`)
さて、今回はクラスと構造体がテーマである。プラグイン・プログラミング(4)でも触れたが、今回はディープに進める予定である。
★POD型構造体を取り扱ってみる
まずは、基本中の基本であるPOD型構造体を取り扱ってみる。
PODとは、Plain-Old-Data………即ち、並に昔ながらのデータである。
つまり基本型やポインタ、あるいはそれらで構成された構造体のことである。
C++による追加要素、即ちクラスや参照、コンストラクタやデストラクタはPODに含まれない。
さて、POD型構造体を取り扱う具体的なソースコードだが………実は、プラグイン・プログラミング(6)で既に触れていたりする(´Д`)
要はDLL側とEXE側に同一の構造体を記述すれば、簡単にDLLとEXEの間で構造体をやりとり(引数にしたり中身いじったり)が可能なのだ。
しかし、実は前回のソースコードではひとつだけ不足している記述が存在する。それはパディングに関する記述である、パディングとは何か?
★パディングとは?
CやC++を勉強してきた者は大抵知っているが、変数とは即ちメモリである。
charは1バイトであるし、intは不定ながらもWin32環境では4バイトであるのは常識だろう。
では、構造体のメモリ上の構造はどうなっているのだろうか? 以下に、表で示してみよう。
struct DATA{ char flag; char name[12]; };
上記のような型では、こうなっていると予想される。
(※注意:分かり易いように4バイト単位で改行してある)
flag | name[0] | name[1] | name[2] |
name[3] | name[4] | name[5] | name[6] |
name[7] | name[8] | name[9] | name[10] |
name[11] | ? | ? | ? |
しかし、VCではこうならない。以下のようになってしまう。
flag | ? | ? | ? |
name[0] | name[1] | name[2] | name[3] |
name[4] | name[5] | name[6] | name[7] |
name[8] | name[9] | name[10] | name[11] |
つまり、隙間が空いているのだ。これを、パディングという。
何故こんな隙間が生じるかというと、コンピュータの処理のし易さをコンパイラが考慮しているからである。
現在主流である32ビットCPUは32ビット単位、即ち4バイト単位でメモリにアクセスするのが最も高速となる。
なので、コンパイラがname[0]に対するアクセスを高速化する為に勝手に4バイト単位で整列させてしまうのである。
よって、異種コンパイラ間で構造体をやりとりする場合、パディングを統一しなければならない。BCCが上のメモリ配置で、VCが下のメモリ配置だと困るのだ。
では、どうすればパディングを統一できるか? これには二つの答えが存在する、プロジェクトの設定を直接いじるか、「プラグマ」を用いてソースコードに直接記述するかだ。
プロジェクトの設定を直接いじるのなら、「構造体のアラインメント」を変更すればOKである。
「プラグマ」を用いてソースコードに直接記述するなら、「#pragma pack(n)」(nは何バイト単位か、1ならパディング無し)とソースコードに記述する。
ここでは詳しく説明しないが、「プラグマ」とはコンパイラに直接与える命令のようなものである。コンパイラに、パディングの有無を指定してやるのが「#pragma pack(n)」である。
ちなみに、前回のプログラムがBCCでもVCでも正常に動いたのは、パディングの有無に左右されないメモリ配置に構造体がなっていたからである。メモリ配置を、以下に示すと
struct Test{ int nMem; char strMem[20]; };
nMem | nMem | nMem | nMem |
strMem[0] | strMem[1] | strMem[2] | strMem[3] |
strMem[4] | strMem[5] | strMem[6] | strMem[7] |
strMem[8] | strMem[9] | strMem[10] | strMem[11] |
strMem[12] | strMem[13] | strMem[14] | strMem[15] |
strMem[16] | strMem[17] | strMem[18] | strMem[19] |
というような感じで、パディング無しでも(4バイト単位の)パディング有りでも間に詰め物が必要無いからである。
(パディング無しと(8バイト単位の)パディング有りなら当然メモリ配置が異なってしまうが、現状ではパディング無しあるいは(4バイト単位の)パディング有りがデファクトスタンダードである)
………とまあ、ややこしく書いてみたワケだが。要は、#pragma pack(1)あるいは#pragma pack(4)とDLLやEXE(の初めに読み込まれるであろう部分)に記述すれば解決するということである。
★POD型+α構造体を扱ってみる
さて、上記でPOD型構造体は(パディングさえ考慮すれば)DLLとEXEの間で使える事が明らかとなった。
では、次はクラスを………と、準備も無しにするのは冷や水に一気に浸かって心停止するようなものである。
今度はPOD型+α構造体を扱ってみよう、αとは即ちメンバ関数の事である。このセクションを読み終われば、POD型+α構造体は扱えるようになる筈だ。
★POD型+α構造体について(1)
さて、再びメモリ配置の話に戻ろう。上記のTestに、メンバ関数をひとつ追加してみる↓
struct Test{ int nMem; char strMem[20]; void Clear(){ nMem = 0; strcpy(strMem, ""); } }
………まあ説明は要らないであろうが、void Clear()という関数を用意してみた。名前通り、メンバ変数をクリアする関数である。
さて、ここでひとつ問題を出してみる。元のメンバ変数のみなTest、及びメンバ関数を追加したTestのメモリサイズは何バイトなのだろう?
答え(メンバ変数のみ):sizeof(Test) = 24
答え(メンバ関数追加):sizeof(Test) = 24
………なんと、1バイトたりとも変わっていないのである。つまり、メンバ関数を追加しようとしまいと同じ。どういうこっちゃ?(´Д`)
これがC++の深いところであり、美しく素晴らしいと思えることである。このようになっているのには、きちんと理詰めされた理由があるのだ。
俺は理詰めで考えて答えを出し、調べることでその答えが正しいものだと知った。どうか、これを読んでいる人にも一度は考えてもらいたい。
理由:
まず、struct Testの中にvoid
Test::Clear()の情報を含めることを考えてみよう。
関数自体を構造体の中に含める:論外、多くの構造体を生成すれば生成するほど無駄が生じてしまうから。
関数ポインタを構造体の中に含める:おしい、確かに無駄は少なくなるが仮想関数でない関数なら各々の構造体が関数ポインタを保持する必要もない。
正解
コンパイラが、void Test::Clear()の呼び出しを該当する関数のメモリアドレスに置き換えている。もちろん、「どの構造体から呼ばれたか」は重要なので
それだけは隠し引数として渡すようにする。具体的に例を挙げると、void
Test::Clear()はvoid Test_Clear(Test* Obj)などと構造体の外部に展開される。
………少し複雑な説明になってしまったが、分かって頂けただろうか?
理詰めで考えれば最も効率的である選択肢を、C++は採っていると理解できた筈だ。
………ここまで書けば、既に聡い人は気付いているだろう。POD型+α構造体をDLLとEXEの間でやり取りするのは、チョー簡単なのである。
理由は単純、メンバ関数が構造体の外部に展開されるならメンバ変数のみの構造体と実質的には変わらないからである。結論だけで書いてしまえば一行である(;´Д`)
コンパイラがメンバ関数を該当する関数のメモリアドレスに置換するということは、DLLではDLL内のvoid
Test::Clear()をEXEではEXE内のvoid Test::Clear()を呼び出すということである。
「なら、メンバ関数の中身をDLLとEXEで変えたらポリモーフィックに扱えね?」、と考える人もいるかもしれないが………そういうことはしないように、バグの原因になるのが見えてるからね(;´Д`)
★POD型+α構造体について(2)
これでPOD型+α構造体については終了したのだが、注意事項がひとつだけ残っている。
DLL側のvoid Test::Clear()はDLL側の標準関数を使用し、EXE側のvoid
Test::Clear()はEXE側の標準関数を使用するということだ。
以前にも書いたが、異種コンパイラ間では同じ標準関数でも内部の構造が同一であるとは限らない。上のTestなら、strcpyが該当する。
同じstrcpyでも、BCCとVCでは内部での処理方法が異なるかもしれない………つまり、完全なる互換性は保証されていないのである。
………ま、内部での処理方法が異なっていても出力する結果が同じなら問題無いが。ただ、以前にも指摘したmalloc/freeやnew/deleteの問題がここで再び生じてしまう。
つまりDLL側でnewしたものをEXE側でdeleteできない、あるいはその逆といった問題が生じるワケだ。これでは完全なる自由とは言いがたい、何とかならないものだろうか?
………何とかなるのである(´Д`) 方法は二種類存在する、このどちらかの方法を用いればDLLとEXEの間で何の不自由もなくPOD型+α構造体を自由にやりとり可能となる。
1.EXE側のnew/deleteをDLL側が呼び出す
DLL側(ソースコード)
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側のnewをNew(newのままではエクスポートできないから)で包み、DLL側で受け取ってDLL側のnewで呼ばせているだけのことである。
こうすれば、DLL側のnewは全てEXE側のnewとなる。即ち、同一のnew/deleteとなるのだから何の問題も無い(配列版のnew/deleteをもオーバーロードするのを忘れずに!)
2.必ず共通の処理となるnew/deleteを行う
DLL側(ソースコード)
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); }
malloc/freeやnew/deleteは内部での処理方法が異なるかもしれない、ならWinAPIで解決しようという方法である。
WinAPIはWindowsの関数なので、BCCでもVCでも内部での処理方法は変わらない。この方法ではGetProcAddressを省ける代わりに、APIを呼ぶという代償を背負うことになる。
以上、これで(7)は終了である。次回こそは本格的に、クラスに取り組む予定である。
はたして、クラスや構造体を自由自在に扱える日が来るのだろうか?………それは、書いてる俺にも分からない(;´Д`)
(※追記:具体的なソースコードが不足しているのでプラグイン・プログラミング(7.5)に掲載した、見たい人は見るがよろし)