DLL(Dynamic Link Library)は実行時にリンクされるライブラリ(プログラムの部品)です
実行時にリンクされるので、うまい作り方ができればプラグイン的な使い方ができます
(実行ファイルを変えることなく、DLLのみ差し替えれば機能変更できる)
今回はVisual C++を使って、DLLを作ってみます
Visual C++を起動し、新規プロジェクトを作ります
とりあえずコンソールを指定しておきます
(後の設定で変更する)
ここでプロジェクト名は自由に決めていいのですが
プロジェクト名は覚えておいてください
「完了」を押さずに「次へ」を押してください
ここでDLLを選択します
空のプロジェクトにも忘れずにチェックを入れてください
チェックを入れたら完了を押してください
以下のファイルを新規作成します
・DLLMain.cpp
・DLLHeader.h
・TestClass.cpp
・TestClass.h
DLLMain.cppです
// DLLMain.cpp #include <windows.h> #include <tchar.h> // DLLのエントリーポイント // DLL読み込み時やDLL解放時に呼び出され // DLLの初期化や後処理を行う BOOL WINAPI DllMain( HINSTANCE hinstDLL, // DLL モジュールのハンドル DWORD fdwReason, // 関数を呼び出す理由 LPVOID lpvReserved // 予約済み ) { switch(fdwReason) { // このDLLの呼び出し元のアプリケーション(プロセス)が // 動的な呼び出し、またLoadLibrary関数で // このDLLを読み込んだときに行う処理(初期化に使う) case DLL_PROCESS_ATTACH: { // 初期化処理 MessageBox(NULL,_T("DLL ロード"),NULL,MB_OK); } break; // このDLLの呼び出し元のアプリケーション(プロセス)が // 終了したあるいはそこからFreeLibrary関数が呼ばれたときに // このDLLはアンロードされる // アンロードされる前に行う処理(後処理に使う) case DLL_PROCESS_DETACH: { // 後処理 MessageBox(NULL,_T("DLL 解放"),NULL,MB_OK); } break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; } return TRUE; }
DLLのエントリーポイントです
プロセスやスレッドの初期化時と終了時、
また、LoadLibrary 関数と FreeLibrary 関数の呼び出し時に、
システム(OS)がDLLMain関数を呼び出します。
上記の方法は明示的な呼び出しですが、
動的な呼び出しのほうがより一般的な使い方です
動的な呼び出しとはDLLと関連付けられたアプリケーションが起動したとき
(より正確にはアプリケーションで最初に書かれたDLL側の関数の呼び出しやクラスのインスタンスが生成がされる前に)
DLL_PROCESS_ATTACHは呼ばれ、呼び出し元のアプリケーションが終了すると
DLLもDLL_PROCESS_DETACHが呼ばれ自動的に破棄されます
使い方としてはDLLの初期化処理や後処理などを行うといいでしょう
(実はDLLMain関数は書かなくてもビルドできます)
DLLHeader.hです
このヘッダーはこのDLLを呼び出すアプリケーションでも必要になります
// DLLHeader.h #pragma once #ifdef TESTDLL_EXPORTS // DLL側プロジェクトで定義されている #define _EXPORT __declspec(dllexport) #else #define _EXPORT __declspec(dllimport) #endif /* TESTDLL_EXPORTS */
他のアプリケーションからDLL側の関数やクラスを呼び出すためには
DLL側でそれらの関数やクラスを一回エクスポートし
呼び出し側ではそれらの関数やクラスをインポートする必要があります
(逆に言えば呼び出し側で直接使わせたくない関数やクラスに関してはエクスポートしません)
エクスポートするには次のようなエクスポートキーワードを
エクスポートする関数やクラスの宣言に付けます
__declspec(dllexport)
インポートするには次のようなインポートキーワードを
インポートする関数やクラスの宣言に付けます
__declspec(dllimport)
普通に作ると同じ関数やクラス宣言なのに
エクスポート用とインポート用の
2つのヘッダーを作る必要がでてきてしまうのですが
エクスポート用とインポート用を共通のヘッダーで済ます方法があります
ここでポイントは次の一行です
#ifdef TESTDLL_EXPORTS // DLL側プロジェクトで定義されている
これはどこで定義(#define)されているのでしょうか?
答えはプロジェクトのプロパティ→プリプロセッサの定義です
ここに書かれているものは同一プロジェクト内で#defineされているのと同じ意味を持ちます
DLLプロジェクトを作ると自動的に
「プロジェクト名(大文字)_EXPORTS」
という定義がされます
他のプロジェクトでは定義がされていないので
DLL側とDLL呼び出しアプリケーション側で切り替えを行います
#ifdef TESTDLL_EXPORTS // DLL側プロジェクトで定義されている #define _EXPORT __declspec(dllexport) #else #define _EXPORT __declspec(dllimport) #endif /* TESTDLL_EXPORTS */
つまり、
共通の#defineした_EXPORTを使うことで
DLL側、呼び出し側でエクスポート、インポートの切り替えが行うことができ
DLL側、呼び出し側共通のヘッダーとして扱うことができます
したがって(エクスポートorインポート)する関数やクラスには_EXPORTを付けます
使い方はTestClass.hで説明します
TestClass.hです
// TestClass.h #pragma once #include "DLLHeader.h" // class エクスポート(インポート)キーワード クラス名 class _EXPORT TestClass { public: TestClass(void); virtual ~TestClass(void); }; // 関数の場合は先頭にエクスポート(インポート)キーワードを付ける _EXPORT void Test(); // エクスポートしない関数やクラスにはキーワードを付けない void InOnlyDLLCall();
DLLHeader.hヘッダーのインクルードを忘れないでください
クラスをエクスポート(インポート)するには
class エクスポート(インポート)キーワード クラス名を指定します
関数をエクスポート(インポート)するには
先頭にエクスポート(インポート)キーワードを付けます
エクスポートしない関数やクラスは何もつけません
この場合、呼び出し側ではこの関数やクラスを使うことはできません
(DLL側内では使える)
TestClass.cppです
// TestClass.cpp #include "TestClass.h" #include <tchar.h> #include <windows.h> TestClass::TestClass(void) { MessageBox(NULL,_T("DLLクラス呼び出し"),NULL,MB_OK); } TestClass::~TestClass(void) { } void Test(){ InOnlyDLLCall(); } void InOnlyDLLCall(){ MessageBox(NULL,_T("DLL関数呼び出し"),NULL,MB_OK); }
特に気を付けることは何もありません
ではビルドしてみましょう
ビルドに成功すると
DLLプロジェクトのDebug(もしくはRelease)フォルダに
DLLファイルとLIBファイルが作成されます
では、早速今作ったDLLを使ってみましょう
新規のプロジェクトを作ります
DLLのヘッダーファイルと作成したLIBファイル、DLLファイルが必要なので
今作ったプロジェクトのフォルダにコピーしてください
#include "TestClass.h" #pragma comment(lib,"TestDLL.lib") int main(){ TestClass testclass; Test(); // エクスポートしてないのでリンカエラーとなる //InOnlyDLLCall(); return 0; }
ビルド→実行して
次のメッセージボックスが出てきたら成功です
ここで気を付けてほしいのはInOnlyDLLCall関数はエクスポートされていないので
呼び出そうとするとリンカーエラーとなります
試しにコメントをはずすしてビルドすると次のエラーが出てきます
error LNKはリンカーのエラーを示しています
void _cdecl InOnlyDLLCall()の横に気味の悪い文字列が出てきます
?InOnlyDLLCall@@YAXXZ
これは関数やクラスの名前修飾と呼ばれ
本当の関数の名前です
@以下は関数の戻り値、引数を示しており
同じ名前の関数で引数、戻り値が違う(オーバーロードされている)場合でも
正しい関数を呼び出しできるのはこれのおかげです
ちなみに、名前修飾の付け方はコンパイラによって異なります
実は作成したLIBファイルにはDLLからエクスポートされた関数、クラスの名前修飾が書かれています
そのために、その情報を元にDLLの実装を実行時にリンクさせ
DLL側の関数やクラスを呼び出すことができるのです
コメントアウトを元に戻して
再度実行ファイルを作ります
実行ファイルを起動するにはDLLが必ず必要となります
(同じフォルダもしくはシステムの環境パスが通ってる箇所の置きます)
さて、
実行(.exe)ファイルを変えずに機能を変えるにはどうしたらよいでしょうか?
ここがプラグインを作るポイントなのですが
名前修飾の定義が変わらなければ
(エクスポートするクラスや関数の定義)
LIBファイルは変わりません
つまり、実行ファイルからのDLL側の関数もしくはクラスの呼び出しに問題はないのです
そこでDLL側の実装ファイルを変更してみましょう
// TestClass.cpp #include "TestClass.h" #include <tchar.h> #include <windows.h> TestClass::TestClass(void) { MessageBox(NULL,_T("DLLクラス呼び出し、差し替えver"),NULL,MB_OK); } TestClass::~TestClass(void) { } void Test(){ InOnlyDLLCall(); } void InOnlyDLLCall(){ MessageBox(NULL,_T("DLL関数呼び出し、差し替えver"),NULL,MB_OK); }
ビルドしてできたDLLファイルを先ほどの実行ファイルフォルダのDLLと差し替えます
(DLLファイルの名前は変えていけないのでそのまま上書きします)
実行ファイルの起動結果は次のようになります
いかがだったでしょうか?
うまく設計すれば、DLLを差し替えるだけでアプリケーションを
バージョンアップさせることができます
これはまさにプラグインです
話はそれますが、COMはこの方法をきちんと体系化したものにすぎません
いちいちアプリケーション本体をビルドをしなくてよくなります
プログラムが大きくなるとアプリケーションのビルドは時間もかかるようになります
DLLを使うことでビルド時間の短縮にもなります
そのほかにも共通の部品として扱えるようになるので
ファイルサイズの削減にもなります
今回のソースファイルは下からダウンロードできます
DLL.zip