MFC拡張DLLの中に作成したクラスでGDI+を使用することになったのですが、
そうなるとGdiplusStartup()やGdiplusShutdown()を呼ばなければいけません。
MFCアプリケーションやMFC標準DLLであれば、
アプリケーションクラスのInitInstance()やExitInstance()で呼べばよいのでしょうけど、
MFC拡張DLLではどのようにするべきなのでしょうか。
DLLを使用するMFCアプリケーション側が呼んでおいてくれることには
依存しないようにしたいです。
たとえば、コンストラクタでGdiplusStartup()、
デストラクタでGdiplusShutdown()を呼ぶようなクラスを作っておき、
DLL内のグローバル変数として置いておく、とかになるのでしょうか?
どういう作りになっているのかわからないのでアレですが、
一般に
1. DLLの初期化及び終了処理は、DllMain()で行います。
第二引数が呼び出された「理由」になります。
C++のオブジェクトなど、プロセスに一つあれば、
十分なインスタンスは、プロセスがアタッチした場合に構築し、
プロセスがデタッチした場合に破棄するのが普通です。
という答えになってしまうのですが、おわかりになるでしょうか。
GdiplusStartup や GdiplusShutdown を呼び出す MyDllInit や MyDllTerminate といっ
た初期化/終了関数(名前は例です)を用意し、それを呼んでもらうお約束にするという
のはいかがでしょうか。
> DLLを使用するMFCアプリケーション側が呼んでおいてくれることには
> 依存しないようにしたいです。
というのは、上記のような案も NG でしょうか。
また、その DLL を MFC 以外のアプリケーションやレギュラー DLL から呼ぶ場合は、CDy
nLinkLibrary による初期化を行うために、初期化関数をエクスポートする必要がありま
す。
http://msdn.microsoft.com/ja-jp/library/h5f7ck28.aspx
そのため、Gdi 以外の要件からも、初期化関数を作っておくと良いのではないかと思いま
す。
上記のページには拡張 DLL では DllMain 関数内で初期化を行うように書いてありますが
、DllMain 自体のルールは拡張 DLL だろうが非 MFC レギュラー DLL だろうが同じなの
で、kernel32.dll がエクスポートする関数以外は呼ぶべきではありません。
http://msdn.microsoft.com/en-us/library/ms682583.aspx
DllMain から呼んではならないということは GdiplusStartup のドキュメントにも明記さ
れていますね。
http://msdn.microsoft.com/en-us/library/ms534077.aspx
> たとえば、コンストラクタでGdiplusStartup()、
> デストラクタでGdiplusShutdown()を呼ぶようなクラスを作っておき、
> DLL内のグローバル変数として置いておく、とかになるのでしょうか?
この方法では GdiplusShutdown が呼ばれることはありませんよね、というのは置いてお
きましょう。
どうせアプリケーション側では明示的に LoadLibrary / FreeLibrary することはないで
しょうから同じことだと思います。
このようなアプローチを取った場合、DllMain に課せられる制約は、グローバルオブジェ
クトのコンストラクタとデストラクタにも適用されます。
従って、行うべきではありませんということになります。
仲澤@失業者さん、aetosさん、情報ありがとうございます。
> 1. DLLの初期化及び終了処理は、DllMain()で行います。
> 第二引数が呼び出された「理由」になります。
説明不足で申し訳ありませんでした。
MSDNのGdiplusStartup()の説明を見ると、「DllMainの中からは呼ぶな」と書かれていま
した。
http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms534077%28v=vs.85%29.aspx
その後ろには、「実行ファイル側に呼んでもらえ」とか「描画処理の中で完結させろ」とか、
対応方法と思われるものが書いてあるのですが、MFC拡張DLLとして作成するときには
もっとスマートな方法がないのかなと思って質問させていただきました。
> この方法では GdiplusShutdown が呼ばれることはありませんよね、というのは置いてお
> きましょう。
このことについてお伺いしたいのですが、
試しに以下のようなクラスを作り、
CGdiplusInit::CGdiplusInit()
{
GdiplusStartupInput input;
m_bResult = (GdiplusStartup(&m_token, &input, NULL) == Ok);
}
CGdiplusInit::~CGdiplusInit()
{
if (m_bResult) {
GdiplusShutdown(m_token);
}
}
唯一のインスタンスをMFC拡張DLL内のグローバル変数として作成し、
デストラクタのGdiplusShutdown()のところにブレークポイントを置いてみたところ、
アプリケーション終了時にブレークはかかるのですが、
これはGdiplusShutdown()が呼ばれているということではないのでしょうか?
それとも、現在のテストプログラム上ではたまたま動くだけで、
このMFC拡張DLLの使われ方によっては正常に動かなくなるということなのでしょうか?
> アプリケーション終了時にブレークはかかるのですが、
あ、そうでしたか。そこまで確認していませんでした。
ただ、アプリケーション終了時まで呼ばれないということは、実質的に意味がないと言え
るのではないかと思います。
アプリケーションが終了するときには、アプリケーションが確保したリソースはすべて解
放されるので、アプリケーションが長時間動き続けるサービスのようなものでなければ、
実質的にリソース リークは発生しないからです。
とはいえ、それを狙って確信的にクリーンアップ処理を省略するのは、褒められた話では
ありません。
そして、そこでデストラクタが呼ばれるとしても、GdiplusShutdown はそこで行うべきで
はありません。
DllMain のドキュメントにこうあります。
> If your DLL is linked with the C run-time library (CRT),
> the entry point provided by the CRT calls the constructors and destructors
> for global and static C++ objects.
> Therefore, these restrictions for DllMain also apply to constructors and
> destructors and any code that is called from them.
http://msdn.microsoft.com/en-us/library/ms682583.aspx
DllMain でやっちゃいけないことは、グローバルオブジェクトのコンストラクタ/デスト
ラクタでもやっちゃダメだよ、ってことです。
おおっ失礼しました。ごめんなさい。
DllMainでは使うなと明示されてますね。
そうなると、aetosさんの案が普通のやり方になりますね。
むりやりやるとすると、EXE側トップフレームのHWNDをDLLに与えて、
DLL側に非表示の子ウインドウを作り、
それに来る WM_CREATEとWM_DESTROYでやるか。
ってな案しか浮かびませんねぇ。
役に立たなくて申し訳ないです。
情報ありがとうございます。
やっぱりaetosさんの言われるように、
初期化関数/後始末関数をDLLからエクスポートし、
実行ファイル側のInitInstance()やExitInstance()から呼んでもらうのを
ルールにするしかないのでしょうかね。
MFC拡張DLLのDllMain()から
AfxInitExtensionModule()やAfxTermExtensionModule()を呼ぶのはセーフで、
GdiplusStartup()やGdiplusShutdown()を呼ぶのはアウトというのは、
MSDNでたまたま見つけていなかったら、知るすべが無いですよね。
実際、DllMain()のDLL_PROCESS_ATTACHにGdiplusStartup()、
DLL_PROCESS_DETACHにGdiplusShutdown()を入れてみても、
特に問題無く動作しているように思えるため、
MSがどういう理由で呼ぶなと言ってるのか、よくわかりませんでした。
ネット上でソースを検索してみると、何件か見つかってしまいますが、
これらはやってはいけない方法ということですよね?
(これを見て、実はMFC拡張DLLではセーフなのかとも期待してしまいました)
https://searchcode.com/codesearch/view/28373358/
http://trac.osuosl.org/trac/envision/browser/Envision/src3/src/Libs/dllmain.cpp?rev=67
解決マークは付けさせていただきますが、
なにか情報がありましたら、引き続きご意見いただけると嬉しいです。
>MSがどういう理由で呼ぶなと言ってるのか、よくわかりませんでした。
これに関する回答はaetosさんの発言の中で既に説明されていますが、
厳密さを無視して、簡単に説明してみます。
まず、
1.DLLは全てLoadLibrary()によってロードされ、EXEのメモリー空間にマップされる。
2.LoadLibrary()が完了する前に当該DLLのエントリーポイントDllMain()が呼び出される
。
は良いですね。次に、DllMain()の説明にあるように
3.DllMain()内でLoadLibrary()を呼び出してはいけない。
わけですが、これは、1.2.の原理によりDLLの循環的参照の可能性があるためですね。
DLLはひとつづつマップしなければならないわけです。
さて、ここからは個人的な推測になりますが、
4.GdiplusStartup()関数は、何らかの他のDLLをロードする可能性がある。
のではないでしょうか。すなわちDllMain()で使うと極めて危険である。
ということですね。ただし、GDI+が参照称する当該のDLLの全てが
既にマップ済みであれば、動いてしまう。
ということになるのではないでしょうか。
説明ありがとうございます。
原理は理解できたような気がします。
MFC拡張DLLに対しても、DLL側で初期化や後始末ができる
安全なタイミングが欲しかったところです。
補足すれば、ユーザーの作成した任意の DLL がロードされる前に、KERNEL32.DLL だけは
、既にロード済みであることが保障されています。
そのため、DllMain 中で KERNEL32.DLL の関数を呼ぶことは、(DllMain の循環が起きな
いという意味においては)安全です(KERNEL32.DLL の DllMain は改めて呼ばれないので
)。
ただし、KERNEL32.DLL の関数なら何を呼んでも大丈夫かというと、そういうことはなく
、例えば「待機オブジェクトを作るのは構わないが、待機関数を呼ぶべきでない」といっ
た制限があります。
そして、「では、何が大丈夫で、何がダメなのか?」と言うと、その完全なリストは提供
されていません。
> 4.GdiplusStartup()関数は、何らかの他のDLLをロードする可能性がある。
現時点であるかどうかという問題と、今後の問題がありますね。
もしひとたび「GdiplusStartup は DllMain から呼んでも安全です」と言ってしまったら
、後で危険なように仕様変更することができなくなってしまいます。
> MFC拡張DLLに対しても、DLL側で初期化や後始末ができる
> 安全なタイミングが欲しかったところです。
拡張 DLL だからできないわけではなくて、DllMain の問題はどの種類の DLL でも発生し
ます。
拡張 DLL だろうが非 MFC DLL だろうが、DLL である以上、その初期化と終了にはそのた
めの関数を提供するという設計が一般的ということですね。
うーん。
MFC 標準 DLL でも、試してみると InitInstance も DLL の CWinApp コンストラクタも
、結局 DllMain から呼ばれているので、DllMain と同じ制限を受けると思うんですよね
。
これ、ドキュメントがまずいんじゃないのかなぁ…。
> MFC 標準 DLL でも、試してみると InitInstance も DLL の CWinApp コンストラクタも、
> 結局 DllMain から呼ばれているので、DllMain と同じ制限を受けると思うんですよね。
> これ、ドキュメントがまずいんじゃないのかなぁ…。
そう言われてみると、思いっきりDllMain()からInitInstance()が呼ばれています。
この時点で、MFC標準DLLでもInitInstance()の位置ではアウトなのでしょうか。
過去に作成したものは特に問題無く動いていたので、疑ってすらいませんでした。
ただ、CWinApp::InitInstance()を辿っていくと、
内部でPathFindExtension()を使ったりしていますが、
これはKERNEL32.DLLではないですよね。
こういうのは大丈夫なのでしょうか。
MFCを使用するということはMFCのDLL(例:MFC90U.DLL等)がマップ済みということなので、
自動的にuser32.dll、advapi32.dll、gdi32.dll等、
必要となる多数のDLLが芋づる式にマップ済みとなります。
ちなみに、自分のMFC MDIのEXEではもはそのDLLの配下に、
22個のDLLがマップ済みになってました。
これらはMFCの初期化コードが暗黙のリンクをしているものだと考えられ、
依存関係は、Depends.exe(デペンデンシィウォーカー)で見ることができます。
この状態ならば、後続のDLL、特に_USERDLLの場合
のDllMain()から何か呼ばれてもOKなのでしょう。
解説ありがとうございます。
#pragma comment(lib)などで暗黙リンクをしているDLLに対しては、
DllMain()内でDLLの関数を呼んでも構わないということなのですか。
こういうのは、実際に正しく動かない状態に遭遇してみないと、
なにがセーフでなにがアウトなのか、直感的には理解できないものです。
勉強させていただきます。