さて、今までの(1)~(3)までで、FileList.cppを除くFileList.h、FileListProc.hと追加noUser.hはダイアログベースのexeファイルのまんまでよいことが分かりましたが、最後のFileList.cppはDLLとなる為にマニュアルでの変更が必要になります。このマニュアルでの変更はやや技術的な話も入りますが、それを知らなくても簡単にDLLは作れます。そんな訳で本文ではプラクティカルに「何をどうすればよい」かだけを書き、その意味や背景、根拠等は注書きするつもりですので、そのようにお読みください。(まぁ、私も完全に理解しているとはいいがたいのであまり信用しないでくださいね...笑)
まず、BCCForm and BCCSkeltonのプログラム作法をおさらいします。それは、
(1)(完成仕様を考えて)BCCForm(やイメージツール)でリソースを作る
(2)出来上がったrcファイル(とRes.hファイル)をもとにSkeltonWizardでプロジェクト名.h、プロジェクト名Proc.hとプロジェクト名.cppファイル(加えてBCCDeveloper用のbdpファイル)でドンガラウィンドウスケルトンを作る。(必要に応じてウィンドウクラスにメンバー変数や関数を追加する等、プロジェクト.hファイルを加工する。)
(3)ウィンドウメッセージ、メニュー、コントロールに対応したメインウィンドウやダイアログのメンバー関数、ユーザー適宜関数をProc.h(やUser.h)ファイルに実装する。
(4)SDIやMDIウィンドウまたはダイアログベースのアプリケーションが完成する。
というものです。事実今回のFileListでも(1)~(3)のブログでダイアログベースのスタンドアローンアプリケーションのFileList.exeが動きます。
これをDLLにするには以下の(5)を行います。
(5)プロジェクト名.cppを、以下の点でDLL用に改造します。
①WinMainの代わりに、"DLLMain"という関数を使用する。(注1)
②DLL化の為の"DLL.h"ファイルを、呼び出されるDLL側のみならず、呼び出しプログラム側でもインクルードする。(注2)
③DLL側で公開する関数を「輸出(Export)」する一方、呼び出しプログラム側でもその関数を「輸入(Import)」する。(注3)
今回のFileListで言えば、
(a)当初SkeltonWizardが作るcppファイルはモードレスダイアログで記述されますが、
(b)まずそれをモーダルダイアログベースにし、
(c)更にDLL化する
というステップを踏みました。(DLL化にモーダルダイアログが必須ということではありません。FileListはそれを使っている間に呼び出したDirectShowを止めたいためにモーダルダイアログにしただけです。DLL化はモードレスダイアログでもウィンドウベースでも全く変わりません。)
(a)<SkeltonWizardによるオリジナル-モードレスダイアログのexeファイル>
//////////////////////////////////////////
// FileList.cpp
//Copyright (c) 02/08/2022 by BCCSkelton
//////////////////////////////////////////
#include "FileList.h"
#include "FileListProc.h"
////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
//2重起動防止
if(!FileList.IsOnlyOne()) {
//ここに2重起動時の処理を書く(下記は1例)
//HWND hWnd = FindWindow("MainWnd", "FileList");
//if(IsIconic(hWnd))
// ShowWindow(hWnd, SW_RESTORE);
//SetForegroundWindow(hWnd);
//SendMessage(hWnd, WM_COMMAND, IDM_OPEN, 0);
//return 0L;
}
//モードレスダイアログを作成Create(hParent, DlgName, DlgProc);
if(!FileList.Create(NULL, hInstance, "IDD_MAIN", ModelessProc))
return 0L;
//メッセージループに入る
return FileList.Loop();
}
//(解説:このように最初はモードレスダイアログで動くようになっていました。)
↓
(b)<モーダルダイアログ化-モーダルダイアログのexeファイル>
//////////////////////////////////////////
// FileList.cpp
//Copyright (c) 02/08/2022 by BCCSkelton
//////////////////////////////////////////
#include "FileList.h"
#include "User.h" //(解説:これはマニュアルで挿入
#include "FileListProc.h"
////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
//ファイルフィルターの初期化
g_LFlt = "プレイリストファイル(*.plf)\0*.plf\0\0";
g_MFlt = "ビデオファイル(*.avi;*.mp4;*.mov)\0*.avi;*.mp4;*.mov\0オーディオファイル(*.mp3;*.wav;)\0*.mp3;*.wav\0\0";
//ファイルパス
g_Path = "C:\\Users\\ysama\\Programing\\Borland C++\\DirectShow\\Debug\\FileList";
//(解説:これらの文字列ポインター変数は関数の引数とするために、スタンドアローンのexeプログラムではここで初期化するようにしました。)
//モーダルダイアログを作成DoModal(HWND, LPCTSTR, DLGPROC, HINSTANCE);
char* cp = (char*)FileList.DoModal(NULL, "IDD_MAIN", ModalProc, hInstance);
MessageBox(NULL, g_File.ToChar(), "戻り値", MB_OK | MB_ICONINFORMATION);
//(解説:モードレスダイアログをモーダルダイアログに変え、戻り値をcpで受け止めるようにしてその確認用のメッセージボックスを付加しました。また(3)で書いた通り、も―^ダルダイアログ化に伴い、OnClose関数にEndModal関数を付加しました。)
return 0L;
}
//(解説:アプリケーションプログラムから呼ばれるDLLの関数に渡される引数と返す戻り値を考えてモーダルダイアログ化しました。)
↓
(c)<DLL化-最終形-モーダルダイアログのDLL>
//////////////////////////////////////////
// FileList.cpp
//Copyright (c) 02/08/2022 by BCCSkelton
//////////////////////////////////////////
#include "DLL.h"
//(解説:説明の通り、DLL化の為のヘッダーファイルをインクルードします)
#include "FileList.h"
#include "User.h"
#include "FileListProc.h"
////////////////
// DLLMain関数
////////////////
int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpReserved) {
//(解説:説明の通り、WinMain関数に代わり、DLLMain関数に替えます。この関数はDLLが呼ばれる度に呼ばれるだけの「エントリーポイント」です。)
//CFILELISTクラスメンバー変数の初期化
FileList.m_hInstance = hInstance;
//(解説:DLLMain関数で引き渡すインスタンスハンドルをメインダイアログのインスタンスに保存します。)
return TRUE;
}
EXPORT char* GetFileList(char* Filter1, char* Filter2, char* FilePath) {
//(解説:このDLL.hで定義された"EXPORT"が「C言語ベースで__stdcall規約の呼び出し関数を外部の利用の為に公開する」という意味を持ちます。)
//ファイルフィルターの初期化
g_LFlt = Filter1;
g_MFlt = Filter2;
//ファイルパス
g_Path = FilePath;
//(解説:予定していた通り、モーダルダイアログ化で初期化していた変数の値に引数を使います。)
//モーダルダイアログとして起動
return (char*)FileList.DoModal(NULL, "IDD_MAIN", ModalProc, FileList.m_hInstance);
//(解説:モーダルダイアログを呼ぶことはexeファイルと変わりません。唯一この関数の戻り値として直接plfファイルのパス、名を返します。)
}
このように変えてゆくだけでSkeltonWizardで作成されたBCCSkeltonプログラムを簡単にDLL化することができます。
「習うより慣れろ」といいます。まずは簡単なダイアログのプログラムを作り、それをDLL化してコツをつかんでほしいと思います。
次回はBatchGoodによるDLL化したFileListのコンパイル方法と共に、FileList.dllを呼び出すサンプルを紹介します。
<<<以下はややめんどーくさい話ですが、気が向いた方は読んでください>>>
注1:「WinMainの代わりに、"DLLMain"という関数を使用」
実行型ファイルの開始アドレスをエントリーポイントというようで、それがウィンドウベースのexeファイルだとWinMain関数になることはご理解の通りです。これがDLLになるとエントリーポイントであることは変わりませんが、DLLプログラムを起動する(DLLファイルがLoadLibraryでロードされる)際に呼ばれる関数となります。(従って、公開されて使用する関数ではありません。)
DLLMainは、
bool WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpReserved)
と定義されていますが、
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
と書かれていることもあります。但し実際にはWINAPI == APIENTRY、HINSTANCE == HMODULEで、場合によっては、
int WINAPI DllEntry(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpReserved)
と書いてもコンパイルされます。
DLLMain関数は、DLLが正常にロードされた場合に真を返します。
"return TRUE; // Successful DLL_PROCESS_ATTACH."
参照先:DLLエントリーポイント関数
次に変数の型宣言でもない"WINAPI"や"APIENTRY"とは何でしょうか?
これらは「呼び出し規約」といわれるもので、C言語やC++言語の特徴である「関数の値渡し("call by value")」(注1-1)の方法の違いを表します。
注1-1:これの反対概念は「関数の参照渡し("call by reference")」と呼ばれ、前者が引数を「先入れ後出し(FILO)」でメモリーのスタックコピーで渡すのに対して、後者はアドレスを渡します。その為、前者は呼び出し側の引数が呼び出し先の影響を受けませんが、後者は影響を受ける点が異なります。
CやC++で代表的な「呼び出し規約」(呼び出し方法)として以下の3つがあります。
(1) C(__cdecl→bcc32cの"-pc"オプションで指定)
(2) stdcall(__stdcall→bcc32cの"-ps"オプションで指定)
「関数(引数1, 引数2, 引数3)」を例にとると、これらは引数を右から左に引数3、引数2、引数1の順にスタックに積んで関数を呼び出します。(1)と(2)の違いはスタック処理規則の違いで、__cdeclでは呼び出し元の関数によって、スタックから引数がポップされますが、__stdcallでは呼び出された関数が、自分の引数をスタックからポップします。従って、異なる呼び出し規約でDLLが呼ばれると暴走することが理解できると思います。
(3) fastcall(__fastcall→bcc32cの"-pm"オプションで指定)
これは引数をスタックではなくCPUのレジスターに入れて関数を呼びますので高速です。DelphiやBCBではお馴染みですよね。
これらの呼び出し規約が特定の必要性から別名(Alias)を受けたのが"WINAPI"や"APIENTRY"で、例えばbcc55のwindef.hでは次のように定義されています。(BCC102ではwindef.h、winbase.h、windows.hには見当たりませんね。どこ行ったのでしょう?)
【bcc55 - windef.hから抜粋】
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
注2:「DLL化の為の"DLL.h"ファイルをインクルード」
先ずは私のDLL.hの中身とコメントにつけた「名前修飾(Name mangling)」の参照先を見てください。
【DLL.h】
/////////////////////////////////////////////////////////////////////////////////////////
// DLL.h for DLL Programs
//名前修飾(Name mangling)
//<Wiki> https://ja.wikipedia.org/wiki/%E5%90%8D%E5%89%8D%E4%BF%AE%E9%A3%BE
//<QIITA> https://qiita.com/nomunomu0504/items/722a2771fef7d8038ceb
//<Visual Studio> https://docs.microsoft.com/ja-jp/cpp/cpp/definitions-and-declarations-cpp?view=msvc-170
/////////////////////////////////////////////////////////////////////////////////////////
#define EXPORT extern "C" WINAPI __declspec(dllexport)
#define IMPORT extern "C" WINAPI __declspec(dllimport)
赤字のEXPORTとIMPORTがextern "c"以降と同義を謂う意味ですが、最初のextern "C"を理解するには前提となる、C++コンパイラーの「名前修飾(Name mangling)」を理解しなければなりません。
この「名前修飾(Name mangling)」とは、C言語のコンパイラーが関数のリンクIDを単純な関数名に基づく参照名称にするのに対し、C++言語のコンパイラーでは関数のオーバーロード(注2-1)が可能であり、「同じ関数名が異なる関数に使用される」ことからコンパイラーがユニークな参照名称を作成することを言います。
注2-1:関数のオーバーロードとは、同一名称の関数でも、引数や戻り値が異なると異なる関数として認識され、コンパイルされることを謂います。以下の架空の関数はすべて別物で、内部的には"@foo$qizc"、"@foo$qv"というような名称で区別されて管理されます。
void foo(int);
char* foo(int);
int foo(int);
int foo(int, int);
int foo(char*);
「extern "C"」宣言は、(C++ではコンパイラーが勝手に名前修飾をするので参照名をうかがい知れないので)本来C言語では外部変数や関数の存在を宣言する"extern"予約語を使い、「(唯一無二の関数名しか許さない)C言語の方式で外部に公開する」ことを意味しているといえます。
参考:extern "C"とは
しかし、これってDLLで外部公開する関数が↑のfooのようにオーバーロードされた関数である場合には「extern "C"」を使ってはならず、C++でファイルリンケージにしなければならないことを意味します。
次の"WINAPI"については「呼び出し規約」で__stdcallのことだと既に説明しました。
最後が"__delspec(dellexport, (または)dllimport)"です。これは簡単に説明がしにくいもののようで、多くのwebサイトでも「おまじない」として使えと言っています。実際これを説明しているのはMicrosoftの英文サイトしかなく、「文法(Grammer)」のところもよく分からないのですが、
The extended attribute syntax for specifying storage-class information uses the __declspec keyword, which specifies that an instance of a given type is to be stored with a Microsoft-specific storage-class attribute listed below.
ということで、「保存するインスタンスのマイクロソフトのスト―レージクラスを特定する拡張属性構文」のようです。まぁ、知っていてもプログラミング上得るものはなく「おまじない」として理解するのが正しいのでしょう。
注3:「DLL側で公開する関数を「輸出(Export)」、「輸入(Import)」」
DLL.hのEXPORTとIMPORTが大体何をしているか、が分かったところで、これをどう使うかになります。
まず、DLLの中で外部からの利用の為に公開したい関数があれば、
EXPORT (戻り値の型宣言) (関数名)(引数...) {
...
}
のように記述します。
【FileList.dllの場合】
EXPORT char* GetFileList(char* Filter1, char* Filter2, char* FilePath) {
...
}
この関数を使用する場合、呼び出し側も相対する関数のプロトタイプ宣言を行う必要があります。
【FileList.dllの場合】
IMPORT char* GetFileList(char*, char*, char*);