読者です 読者をやめる 読者になる 読者になる

syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

AFX_EXT_API/AFX_EXT_DATA/AFX_EXT_CLASSの罠

C++ プログラミングTips MFC

(これは2012-06-05に書いた故OCNブログの記事を移植したものです)

Visual C++MFC拡張DLLを使って名前空間レベルのグローバル関数・グローバル変数やクラスそのものをエクスポートするとき、

extern AFX_EXT_API void MyGlobalFunc();
extern AFX_EXT_DATA int g_myGlobalVariable;

class AFX_EXT_CLASS MyClass { ... };

な感じでマクロを使って宣言部を修飾してやれば簡単にエクスポートできます。

原理は単純で、DLLをビルドするときは _AFXEXT マクロが定義されるので、afxv_dll.h の定義を見れば分かるように、

AFX_EXT_API マクロが AFX_API_EXPORT(つまり __declspec(dllexport))になり、
AFX_EXT_DATA マクロが AFX_DATA_EXPORT(つまり __declspec(dllexport))になり、
AFX_EXT_CLASS マクロが AFX_CLASS_EXPORT(つまり __declspec(dllexport))になります。

逆に、DLLを利用するEXE側をビルドするときは_AFXEXTマクロが定義されないので、

AFX_EXT_API マクロが AFX_API_IMPORT(つまり __declspec(dllimport))になり、
AFX_EXT_DATA マクロが AFX_DATA_IMPORT(つまり __declspec(dllimport))になり、
AFX_EXT_CLASS マクロが AFX_CLASS_IMPORT(つまり __declspec(dllimport))になります。

ただし、「グローバル変数」や「静的メンバー変数を含むクラス」をDLLエクスポートする場合、うかつにこれらのマクロを使うと罠にハマるので注意。

例えば、

  • MFC アプリ MyTestApp00.exe が、MFC 拡張 DLL の MyTestLib01.dll に AFX_EXT_DATA や AFX_EXT_CLASS でエクスポートされた「グローバル変数」や「静的メンバー変数を含むクラス」を直接参照しようとした場合

は問題ないんですが、

  • MFC 拡張 DLL の MyTestLib02.dll が、別の MFC 拡張 DLL の MyTestLib01.dll に AFX_EXT_DATA や AFX_EXT_CLASS でエクスポートされた「グローバル変数」や「静的メンバー変数を含むクラス」を参照しようとした場合

にNGとなります(外部参照が未解決になる)。グローバル関数に対するAFX_EXT_APIや、静的メンバー変数を含まないクラスに対するAFX_EXT_CLASSではこの現象は起きません。やはりグローバル変数および静的メンバー変数をMFC拡張DLLからインポートする際の固有問題らしいんですが、これを解決するためには、まず

#ifdef MY_TEST_LIB01_DLL_BUILD
#define MY_TEST_LIB01_DLL_API  AFX_DATA_EXPORT
#else
#define MY_TEST_LIB01_DLL_API  AFX_DATA_IMPORT
#endif

を公開ヘッダーの先頭に定義して、さらにMyTestLib01.dllのプロジェクト シンボルもしくはstdafx.hにMY_TEST_LIB01_DLL_BUILDを定義して、AFX_EXT_DATAやAFX_EXT_CLASSを使う代わりにMY_TEST_LIB01_DLL_APIを使って、エクスポートしたい「グローバル変数」あるいは「静的メンバー変数を含むクラス」を修飾してやる、という、いつもWin32 DLLを作るときにやってるのとほぼ同じ方法を使う必要があります。

ちなみに名前空間レベルの「グローバル変数」をDLL公開しようとするのはかなり危うい設計で、そんなことをするくらいだったら「クラスにstaticメンバー変数とそのstaticアクセッサを定義して、そのクラスをDLL公開する」方法*1のほうがまだマシなんですが、「古いコードをDLLに切り分けるんだけど、リファクタリングするよりも先にまずそのまま移植しないといけない」ようなときには、グローバル変数をどうしてもそのまま使わざるをえないことがあったりします。

しかしC/C++はABI仕様が処理系依存なので、ソースコードはともかくバイナリレベルのコード再利用は苦労します。VC#などは最初からコードをバイナリレベルで再利用することを前提として.NET Framework基盤とともに言語仕様が作られている*2ので、自作のクラス ライブラリをDLL公開するなんてのはアクセス指定子をpublicにするだけでよかったりしますが、それに引き替えC/C++の再利用性の低さは絶望的なレベルです。特にC++ネーム マングリング規則が処理系依存なため、クラスの再利用なんてのは同一処理系内でないかぎりおよそ不可能です。また、malloc/free, new/deleteといったヒープメモリの管理はコンパイラの種類はもちろんコンパイラのバージョンごとですら異なるので、例えば古いコンパイラで作ったDLLの内部でnewしたオブジェクトを、新しいコンパイラで作ったEXE側で直接deleteするようなことはできません。こういったABI境界(DLL境界)を超えてヒープオブジェクトをやりとりする場合、メモリの生成と破棄に関する一貫した設計が必要となります。もちろんこれは悪いことばかりではなく、異なるバージョンのコンパイラ間でオブジェクトコードやバイナリの互換性を持たせないことで、古いバージョンに対する互換性を気にすることなくコード最適化の改善をバージョンごとに進めることができるというメリットもあるのですが……

*1:あるいは「staticローカル変数を定義してそのアドレスもしくは参照を返すグローバル関数もしくはstaticメンバー関数をDLL公開する」方法もあります。C/C++でシングルトンを実現するのによく使われる手法です。

*2:もともとC/C++のバイナリ再利用性の低さを解決するために開発されたのがCOMで、そして.NETはCOMの概念をさらに簡素化・昇華した技術です。