これまで、DLLの関数をエクスポートするために__declspecキーワードを 使用しましたが、関数のエクスポートはdefファイルを利用することでも可能です。 defファイルによるエクスポートを採用した場合、 エクスポート関数名にキーワードを付ける必要はなくなります。 defファイルをプロジェクトに追加する方法については、こちらを参照してください。
defファイルは、テキスト形式で記述することになります。 最初から記述されているLIBRARYという文字列は、defファイル専用のキーワードです。 このキーワードには、DLLの名前を指定します。 作成したDLLがmydll.dllである場合は、mydllとなっているでしょう。 デフォルトでは、defファイルにはLIBRARYしか含まれていません。 これでは関数をエクスポートできないため、新たなキーワードを指定することになります。
LIBRARY mydll EXPORTS Swap
EXPORTSというキーワード以下に、エクスポートしたい関数名を列挙します。 上記の例であれば、Swapという関数をエクスポートすることを意味しています。 これで、プロジェクトをビルドするとDLLのエクスポートセクションには、 Swapが含まれることになり、EXEはSwapを呼び出すことができます。 また、先に述べたように、defファイルに指定した関数には名前装飾が施されません。
defファイルでは、関数を序数(ordinal)でエクスポートすることも可能です。 序数とは関数の番号のようなもので、以下のように指定します。
LIBRARY mydll EXPORTS Swap @1
このようにすると、Swap関数の序数は1となります。 関数に序数を明示的に割り当てることで、以下のような呼び出しが可能となります。
GetProcAddress(hmod, (LPSTR)1);
この場合、GetProcAddressは序数が1である関数にリンクしようとします。 当然ですが、EXEはSwap関数の序数が1であることを知っていなければなりません。 序数によるリンクは文字列の比較を行わない分、高速であるといわれています。 序数の指定がなかった場合はリンカが適当に序数を決定します。
最後に、defファイルでのコメントの書き方について説明します。
;サンプルです。 LIBRARY mydll EXPORTS Swap
セミコロンが書かれている行はコメントと見なされ、リンクエラーとなることはありません。 コメントを書くことでDLLの目的が確認しやすくなります。
今回のプログラムは、defファイルによって関数をエクスポートします。 defファイル、ヘッダーファイル、ソースファイルの順で以下に示します。
;関数のエクスポートは、defファイルで行うこともできます。 ;EXPORTSキーワード以下に、エクスポートしたい関数名を列挙します。 LIBRARY mydll EXPORTS Swap @1 SwapDouble
続いてヘッダーファイルです。
#ifndef DLLAPI #define DLLAPI extern "C" __declspec(dllimport) #endif DLLAPI BOOL Swap(int *lp1, int *lp2); DLLAPI BOOL SwapDouble(double *lp1, double *lp2);
ソースファイルです。
#define DLLAPI #include <windows.h> #include "mydll.h" BOOL IsValidAddress(LPVOID lp1, LPVOID lp2); BOOL Swap(int *lp1, int *lp2) { int tmp; if (IsValidAddress(lp1, lp2)) return FALSE; tmp = *lp1; *lp1 = *lp2; *lp2 = tmp; return TRUE; } BOOL SwapDouble(double *lp1, double *lp2) { double tmp; if (IsValidAddress(lp1, lp2)) return FALSE; tmp = *lp1; *lp1 = *lp2; *lp2 = tmp; return TRUE; } BOOL IsValidAddress(LPVOID lp1, LPVOID lp2) { return lp1 == NULL || lp2 == NULL; }
defファイルより、今回のプログラムが2つの関数をエクスポートすることが分かります。 また、Swap関数には1という序数を設定しているため、Swap関数においては序数によるリンクも可能となります。 自作関数のIsValidAddressは内部で使われる関数であるため、 defファイルにもヘッダーファイルにも含める必要はありません。
これまで作成してきたDLLには、EXEへの依存性がないという大きな利点があります。 DLLはEXEによって利用されるものですが、今回作成したDLLの関数は特定のEXE用に用意された関数ではありません。 言い換えれば、どのEXEからも利用できる関数ということになるでしょう。 今日では、このような再利用可能なDLLが多数存在するおかげで、 多くのアプリケーションは快適に動作しています。 たとえば、PNGファイルの画像を表示しようと考えたとき、 それを成すにはPNGの内部構造についての知識が必要のように思えますが、 もしPNG入出力DLLというものが配布されていれば、それを借りることができます。 こうすれば、プログラマが覚えることはそのDLLの関数だけになり、 PNGについての詳細を知らずにPNGを扱うことができます。 これは一重にPNG入出力DLLが再利用可能として設計されているからこそ可能なのです。 再利用可能なDLLを配布するには、DLLとヘッダーファイル、 さらに暗黙的リンクを考慮する場合はインポートライブラリも用意したほうがよいでしょう。
再利用可能なDLLの開発者は、DLLが他プロセスのアドレス空間を使用しているという点を十分に意識しておくべきです。 たとえば、DLL内でメモリを確保する関数を呼び出すことは、そのプロセスのアドレス空間を消費することを意味しますから、 メモリの開放を忘れるようなことはあってはなりません。 また、むやみにUIを表示するとコードの進行をブロックすることになりかねないため、 これも極力避けるべきといえます。 そして最も注意しなければならないのは、メモリアクセス違反などによる例外の発生です。 DLL内でこのような例外を発生させることはプロセスの強制終了につながる可能性がありますから、 十分に注意する必要があります。
rundll32.exeについて |
DLLに実装したエクスポート関数をテストするのは多少の手間を要します。 たとえば、そのDLLを呼び出すEXEを別途作成することがあるでしょう。 rundll32.exeは、特定のDLLの関数を呼び出すことに特化したEXEであるため、 これを起動することで関数のテストを簡単に行うことができます。 次に、コマンドプロントからrundll32.exeを起動する例を示します。 rundll32.exe C:\mydll.dll, EntryPoint argument rundll32.exeの後にDLLのフルパスを指定し、カンマで区切った後に呼び出したい関数名を指定します。 そして、その後には関数の引数を指定します。 rundll32.exeで呼び出す関数は、次のようなプロトタイプでなければなりません。 void __stdcall EntryPoint(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow) { MessageBoxA(NULL, NULL, lpszCmdLine, MB_OK); } 関数名は自由に決定することができますが、プロトタイプは上記のようになっていなければなりません。 呼び出し規約は__stdcallでなければならないため、 defファイルによるエクスポートで名前装飾を避ける必要があります。 引数はlpszCmdLineを通じて渡され、先のコマンドプロントであればargumentという文字列が表示されます。 rundll32.exeは最初にUNICODE版の関数(上記であればEntryPointW)を探すため、 引数をUNICODE文字列で受け取りたい場合は、関数にWを付けてLPWSTR型を受け取るようにします。 rundll32.exeに特定の関数を実行させるというのは、ある意味でとても凄いことです。 その関数内で何らかの例外が発生したとしても、強制終了するプロセスはrundll32.exeですから、 自プロセスは全く影響を受けません。 また、rundll32.exeは関数の処理を終えると直ちに終了しますから、 ある処理をrundll32.exeが行ったという形跡が残りにくいという側面も持っています。 たとえば、rundll32.exeがプロセスを作成する関数を呼び出すと、 そのプロセスの親プロセスはrundll32.exeになるわけですが、 rundll32.exe自体が直ちに終了してしまうため、 新しいプロセスはどのプロセスが作成したのかが分からなくなります。 |