概要

DLLを動的リンクで呼び出すと異なる点は、DLL側に派生クラスを作成し、 EXE側から呼び出しています。サンプルでは、共通のヘッダーファイルbaseclass.hで基底クラスBASEを定義しており、 BASEクラスをNEKO.CPPの中でNEKOクラス、INU.CPPの中でINUクラスに派生させています。
たとえばNEKOオブジェクトを作成したい場合は、NEKO.DLLをロードし、base_init関数を呼び出し、 NEKOオブジェクトを作成しそのポインタを得ます。
オブジェクトからメンバー関数を呼び出せるようにするために 、基底クラス(BASE)のメンバー関数をすべて仮想関数(virtual)とします。(baseclass.hで定義)
あとは、基底クラスへのポインタを使って、派生クラスのメンバー関数を呼び出すことができます。

C++における仮想関数の実装メカニズム

どうして、EXEファイルのソース内で宣言されていない派生クラスのメンバー関数にアクセスできるのでしょうか。ここで、C++における仮想関数の実装メカニズムを見てみましょう。
virtual関数を使うとそのオブジェクトに隠しメンバーが作成され、これがメンバー関数へのポインターを定義した配列へのポインターとなっています。 この隠しメンバーはオブジェクトの一番若い番地に作成されます。 これを使うことにより、基底クラスへののポインタを使って派生クラスのメンバー関数を実行することが可能となります。 サンプルのbase.cppの 「 // これより下のコードは 」より下のコードは、隠しメンバーを使って派生クラスのメンバー関数を呼び出す方法を記述しています。 (コンパイラによって実装が異なる可能性があるのでこのような記述はしないでください。あくまでもこのように実装されているという説明用です。) もちろんVisual C++ 2008/2013 の32bit/64bitで正常に動作します。
BASE * NEKOオブジェクト int vftable* vftable 仮想関数テーブル MEKO::put() MEKO::putZ() SVGの代替画像

テスト環境

コンパイラ

Visual C++ 2008 Standard 32/64bit
Visual C++ 2013 Express 32/64bit

プロジェクトの作成

inu.cpp Win32プロジェクト DLL
neko.cpp Win32プロジェクト DLL
base.cpp Win32プロジェクト Windowsアプリケーション
      inu、nekoプロジェクトをbaseソリューションに追加

実行環境

Windows 8.1 Enterprise 64bit
Windows 7 EnterPrise Service Pack 1 64bit
Windows Vista Ultimate Service Pack 2 32bit
Windows XP Professional Service Pack 3 32bit

プログラムソースの概要

base.cpp

_tWinMain関数

DLLは、プログラム内で明示的にファイル名を指定してLoadLibrary APIで(inu.dll、neko.dll)ロードします。
DLL内の関数の呼び出しは、GetProcAddress APIで関数へのポインタを取得し、変数に保存します。
ポインタを使って関数を呼び出します。第2引数に関数名を渡します。GetProcAddress APIの関数名はANSI文字列である必要があります。(UNICODE版は存在しない)  
C++では同じ関数名で引数の違うの関数を作成でき、それらの区別のため関数名の後ろが引数の型等で修飾されます。
呼び出すためには、修飾後の正確な関数名が必要となります。
一般的には関数名を調べるのは煩わしいので、関数名が修飾されないようにDLL側の関数名に extern "C" を付加します。
INUクラスと、NEKOクラスのオブジェクトを作成するためにDLL内のbase_init関数呼び出します。戻り値が動的に作成されたオブジェクトに対するポインタです。
以降、INUクラスとNEKOクラスのオブジェクトへのポインタを介してDLL内の関数を呼び出すことができます。
ここでは仮想関数put()を呼び出します。
次に、仮想関数の実装をテストするために、オブジェクトのポインタが指し示す、最初の要素を取り出します。 これが仮想関数テーブルへのポインタ(vftable)です。
仮想関数テーブルを用いてINUクラスのput関数とputZ関数を呼び出します。
オブジェクトの作成は、メンバー関数を用いませんでしたが、解放は仮想関数として実装しているデストラクタを使用します。
最後にFreeLibrary APIによりDLLを解放します。

baseclass.h

基底クラスBASEを定義しています。
コンストラスタ以外は仮想関数として定義しています。

inu.cpp、neko.cpp

DLLファイルのソースです。
基底クラスBASEは、インクルードファイルbaseclass.hで定義されています。
DllMain関数以外は、他のプログラムから使用できるように関数をエクスポートする必要があります。 ここではマクロEXPORTを定義して各関数で使用しています。
#define EXPORT  extern "C"  __declspec(dllexport) 

DllMain関数

プロセスやスレッドの初期化時と終了時、また、LoadLibrary 関数と FreeLibrary 関数の呼び出し時に呼び出されます。

base_init関数

派生されたクラスオブジェクトをnew演算子により動的に確保し関数の戻り値として返します。
オブジェクトの作成だけメンバー関数を使わないのは、オブジェクトがない状態ですので仮想関数は使えないし、 派生クラスの定義はbase.cpp側ではわからないので直接コンストラスタを呼び出すことができないためです。

INUクラス

BASEクラスを継承してINUクラスを定義しています。
コンストラスタ以外の関数は、すべて基底クラスで仮想関数として定義されています。
各メンバー関数は、動作確認のためにMessgeBoxを記述しています。

プログラムソース

base.cpp

//       動的ロードDLLに派生クラスを実装した簡単なサンプル
//      Visual C++ 2008/2013    32/64bit

#include <windows.h>
#include <tchar.h>
#include "baseclass.h"


int WINAPI _tWinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst,TCHAR* lpsCmdLine, int nCmdShow){
        HMODULE neko_hMod,inu_hMod;

        neko_hMod = LoadLibrary(TEXT("neko.dll"));
        inu_hMod = LoadLibrary(TEXT("inu.dll"));

        BASEFunc base_init_func;
        base_init_func = (BASEFunc)GetProcAddress(neko_hMod, "base_init");
        if (base_init_func == NULL){
                FreeLibrary(neko_hMod);
                FreeLibrary(inu_hMod);
        }

        BASE* nekop;
        nekop=base_init_func(); //      NEKOクラスを作成して初期化後、オブジェクトを返す
        nekop->put();                        //      オブジェクトには仮想関数へのポインタがあるためDLL内にもアクセス可能

        base_init_func = (BASEFunc)GetProcAddress(inu_hMod, "base_init");
        if (base_init_func == NULL){
                FreeLibrary(neko_hMod);
                FreeLibrary(inu_hMod);
        }

        BASE* inup;
        inup=base_init_func();  //      INUクラスを作成して初期化後、オブジェクトを返す
        inup->put();                 //      オブジェクトには仮想関数へのポインタがあるためDLL内にもアクセス可能
        
        BASE* basep;

        basep=nekop;
        basep->put();        //      NEKO::put()が呼び出される
        basep=inup;
        basep->put();        //      INU:put()が呼び出される

//      これより下のコードは仮想関数の実装方法のテストのために記述されています。

        MessageBox(0,TEXT("クラスの仮想関数テーブルを直接アクセスして呼び出します。C言語に変換するとこんな感じかなぁ"),TEXT("これより下のコードは動作説明用なのでよい子はまねをして記述しないように"),MB_OK);

        PVOID *op=(PVOID *)basep;       //      INUオブジェクトへのポインター
        BASEFunc_BASEP* vftable=(BASEFunc_BASEP*)op[0]; //      オブジェクトの最初の要素(隠しメンバー vftable)が仮想関数へのポインタへのポインター
        vftable[0](basep);      //仮想関数へのポインタを使って実行。basep->INU::put(); と同じ
        vftable[1](basep);      //仮想関数へのポインタを使って実行。basep->INU::putZ(); と同じ
//      仮想関数の実装方法のテストのために記述は終了しました。
        delete nekop;
        delete inup;

        FreeLibrary(neko_hMod);
        FreeLibrary(inu_hMod);
        
        return (int)0;
}

baseclass.h

#define EXPORT extern "C" __declspec(dllexport)

class BASE{
public:
        virtual void put(void)=0;
        virtual void putZ(void)=0;
        virtual ~BASE(){};
};

EXPORT BASE* base_init(void);

typedef BASE* (*BASEFunc)();
typedef void (*BASEFunc_BASEP)(BASE* p);

inu.cpp

//       動的ロードDLLに派生クラスを実装した簡単なサンプル(DLL側)
//      Visual C++ 2008/2013    32/64bit

#include <windows.h>
#include <tchar.h>
#include "../../base/base/baseclass.h"

int WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, PVOID pvReserved){
    return TRUE;
}

class INU : public BASE{
        int a;
public:
        INU(){
                a=1;
                MessageBox(0,TEXT("INU::INU()"),TEXT("INU_DLL.DLL"),MB_OK);
        }
        void put(void){
                MessageBox(0,TEXT("void INU::put(void) 呼び出しに成功しました。"),TEXT("INU.DLL"),MB_OK);
        }
        void putZ(void){
                MessageBox(0,TEXT("void INU::putZ(void) 呼び出しに成功しました。"),TEXT("INU.DLL"),MB_OK);
        }
        ~INU(){
                MessageBox(0,TEXT("INU::~INU()"),TEXT("INU.DLL"),MB_OK);
        }

};

//      関数の呼び出し形式にCALLBACK等をつけずにEXPORT(extern "C")を付け名前が勝手に変換されないようにするのがミソ

EXPORT BASE* base_init(void){
        return new INU;
}



neko.cpp

//       動的ロードDLLに派生クラスを実装した簡単なサンプル(DLL側)
//      Visual C++ 2008/2013    32/64bit

#include <windows.h>
#include <tchar.h>
#include "../../base/base/baseclass.h"

int WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, PVOID pvReserved){
    return TRUE;
}

class NEKO : public BASE{
        int b;
public:
        NEKO(){
                b=2;
                MessageBox(0,TEXT("NEKO::NEKO()"),TEXT("NEKO_DLL.DLL"),MB_OK);
        }

        void put(void){
                MessageBox(0,TEXT("void NEKO::put(void) 呼び出しに成功しました。"),TEXT("NEKO.DLL"),MB_OK);
        }
        void putZ(void){
                MessageBox(0,TEXT("void NEKO::putZ(void) 呼び出しに成功しました。"),TEXT("NEKO.DLL"),MB_OK);
        }
        ~NEKO(){
                MessageBox(0,TEXT("NEKO::~NEKO()"),TEXT("NEKO.DLL"),MB_OK);
        }
};

//      関数の呼び出し形式にCALLBACK等をつけずにEXPORT(extern "C")を付け名前が勝手に変換されないようにするのがミソ

EXPORT BASE* base_init(void){
        return new NEKO;
}
        

ソースファイルと実行ファイルのダウンロード

ダウンロード subclassdll.zip(114kByte)
ZIPファイルに含まれるファイル
base.cpp
baseclass.h
base.exe
inu.cpp
inu.dll
neko.cpp
neko.dll