Cとの連係 その3 Windows DLL

なんか方向性がマニアックになってる気がしますが, まあそれはそれとして.



WINUSERAPI

MessageBeep()の詳細を見てみましょう. この関数はwinuser.hで

    WINUSERAPI BOOL WINAPI MessageBeep(UINT uType);

と定義されています. WINAPIはwindef.hの中で, WINUSERAPIはwinnt.h内で

    #define WINAPI __stdcall
    #define WINUSERAPI DECLSPEC_IMPORT
    #define DECLSPEC_IMPORT __declspec(dllimport)

となっています. __stdcallはすでに説明したので __declspec(dllimport)について説明します. ただ, その説明に入る前に通常のDLLについて調べてみましょう. テストDLL1にソースを置きました. このままでは

    外部シンボル "_funcA" は未解決です

というエラーメッセージが出ますが, dll.defのセミコロンを消すとコンパイルできます.

LIBRARY dll
DESCRIPTION "sample dll"
EXPORTS
;   funcA
    funcB

DLLの中のどの関数を外部から見せるかをdll.defによって制御できることが分かります. さて, 実は__declspec(dllexport)命令を使うとdll.defを使わないですみます. テストDLL2のように関数の前につければよいのです.

    __declspec(dllexport) int funcA(int a, int b);
    __declspec(dllexport) int funcB(int a, int b);

このときのアセンブリ言語出力を見てみましょう.

    push    7
    push    5
    call    _funcA

何も問題ないように見えますが, デバッグ実行で混合モードにしてステップ実行してみてください. call _funcAのところで[PF11]を押すと

    jmp     dword ptr [__imp__funcA (004070b4)]

と表示されました. この後もう一度[PF11]を押すと

3:    int funcA(int a, int b)
4:    {
5:        return a + b;
          mov         eax,dword ptr [esp+8]
          mov         ecx,dword ptr [esp+4]
          add         eax,ecx
6:    }

となって正しいfuncAの場所に飛んできました. なぜこのようになっているのでしょう. 実はDLL呼び出し元がfuncAのアドレスを知らないのでテーブルを用意しておき, 実行時にそこのアドレスだけ解決してアドレスを書き込むということをしているからなのです.

この方法だとまわりくどく, 若干コードサイズと処理時間が増加してしまいます. これを回避する手段として前述の__declspec(dllimport)が存在するのです.
このキーワードはコンパイラにDLL内の関数が存在することを知らせます. ヘッダを次のように変更します.

#ifdef DLL_IMPLETATION
    #define DECL_SPEC __declspec(dllexport)
#else
    #define DECL_SPEC __declspec(dllimport)
#endif
DECL_SPEC int funcA(int a, int b);

DLL_IMPLETATIONはdll.cの中で定義しておきます. こうするとDLLを利用する側は__declspec(dllimport)つきでコンパイルされます. 結果を見てみましょう.

22:       funcA(a, b);
          mov         esi,dword ptr [__imp__funcA (004070b4)]
          push        7
          push        5
          call        esi

このcall文は

          call        dword ptr [__imp__funcA (004070b4)]

と同じ意味の間接ジャンプです. Windows APIたちは必ずDLLがあることが前提なのでこのキーワードがついてます. なお, DLLの明示的な呼び出しLoadLibrary()を使う場合は __declspec(dllimport)の効果はありません. これがあろうとなかろうと間接ジャンプになります(testdll3).

         ; esiにはGetProcAddress(module, "funcA");の返り値が入っている.
         push        7
         push        5
         call        esi

御意見は光成滋生<herumi@nifty.com>までお願いします