ATL::CAtlExeModuleTを使ったコードを様々なバージョンのVisual C++でコンパイルしていたところ、非互換な変更を見つけました。

問題を見つけたコードとその症状

次のコード、Visual C++のバージョンを上げると動かないという現象に遭遇しました。

#include <iostream>
#include <windows.h>
#include <atlbase.h>
 
#define nullptr NULL
 
class Module : public ATL::CAtlExeModuleT<Module>
{
public:
  HRESULT Run(int showCmd)
  {
    ATL::CComPtr<IUnknown> obj;
    HRESULT hr = CoCreateInstance(CLSID_GlobalOptions, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&obj));
    std::cout << std::showbase << std::hex << hr << std::endl;
    return hr;
  }
 
  int WinMain(int showCmd)
  {
    // 実際には、これ以外にも前処理や後処理がある。
    return static_cast<int>(Run(showCmd));
  }
};
 
Module module;
 
int main()
{
  return module.WinMain(SW_SHOW);
}

Visual C++ 2008までなら、CoCreateInstanceは成功します。ところが、これをVisual C++ 2010以降でコンパイルすると0x800401f0、すなわちCO_E_NOTINITIALIZEDとなってしまいました。

原因・考察

CAtlExeModuleTのソースを見たところ、Visual C++ 2008から2010の間で変更されていることが分かりました。

  • VC++ 2008まで: CAtlExeModuleTのコンストラクタ・デストラクタで、メンバー関数InitializeComとUninitializeComを呼び出している。
  • VC++ 2010から: CAtlExeModuleTのメンバー関数WinMainで、InitializeComとUninitializeComを呼び出している。

以下のようなわけで、この変更は好ましいと考えています。

  • メンバー関数InitializeComとUninitializeComが中で呼び出している、CoInitializeExおよびCoUninitialize関数は、スレッド単位の初期化・終了処理の関数だからです。
  • 最初に提示したコードのように、CAtlExeModuleT(の派生クラス)は、ほとんどの場合グローバル変数として使われます。main関数を実行したスレッド以外でstd::exit関数を呼び出すと、そのスレッドでデストラクタが実行されます。

したがって、Visual C++ 2008までのCAtlExeModuleT実装では、CoInitializeExを実行したことと異なるスレッドでCoUninitializeを実行してしまう可能性がありました。Visual C++ 2010以降は、メンバー関数WinMainにこの処理が移動したので、この問題が解消されたわけです。

修正後のコード

というわけで、上記のコードは次のように修正することとしました。

#include <iostream>
#include <windows.h>
#include <atlbase.h>
 
class Module : public ATL::CAtlExeModuleT<Module>
{
public:
  HRESULT Run(_In_ int nShowCmd)
  {
    ATL::CComPtr<IUnknown> obj;
    HRESULT hr = CoCreateInstance(CLSID_GlobalOptions, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&obj));
    std::cout << std::showbase << std::hex << hr << std::endl;
    return hr;
  }
 
  int WinMain(int showCmd)
  {
    // 実際には、これ以外にも前処理や後処理がある。
    return ATL::CAtlExeModuleT<Module>::WinMain(showCmd);
  }
};
 
Module module;
 
int main()
{
  return module.WinMain(SW_SHOW);
}

というわけで、CAtlExeModuleTの非互換な変更についての話でした。これ自体は結構前から見つけていて、そのうちブログに書こうと思っていました。前回、それとは無関係にCAtlExeModuleTについて取り上げた(ATL::CAtlExeModuleT::WinMain関数を使う例)ので、その流れでこれを書くことにしたというわけです。

スポンサード リンク

この記事のカテゴリ

  • ⇒ ATL::CAtlExeModuleTのバージョン非互換を見つけた話
  • ⇒ ATL::CAtlExeModuleTのバージョン非互換を見つけた話