本記事は、COM Advent Calendar 2014 – Qiitaの13日目の記事です。


STAはメッセージループの元で動作します。

  • 他のアパートメントからのメソッド呼び出しはウィンドウメッセージで受信します。
  • 他のアパートメントのメソッド呼び出しを行っている間、モーダルなメッセージループが回ります。

これが組み合わさると、1スレッドのみでありながら、あるメソッドの呼び出し中に、他のメソッド呼び出しを受け付けること(再入)があります。

そのことを見てみましょう。

STAからSTAへのメソッド呼び出し

こんなコードを書きました。適当なISequentialStream実装クラスHogeを作り、1つ目のSTA上にオブジェクトを作成します。2つ目のSTAスレッドからその中のメソッドReadを呼び出しています。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <thread>
#include <tchar.h>
#include <windows.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
ATL::CComGITPtr<ISequentialStream> g1;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public ISequentialStream
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(ISequentialStream)
  END_COM_MAP()
 
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(Write)(
    _In_reads_bytes_(cb) const void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbWritten) override
  {
    return E_NOTIMPL;
  }
};
 
void worker(DWORD mainThreadId)
{
  // 2つ目のSTAを作る。
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return;
 
  try
  {
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g1.CopyTo(&s));
 
    // 1つ目のSTAのオブジェクトのメソッドを呼び出す。
    char buf;
    s->Read(&buf, sizeof buf, nullptr);
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
 
  PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
}
 
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
  // 1つ目のSTAを作る。
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return 1;
 
  {
    ATL::CComObjectStackEx<Hoge> obj;
    g1 = &obj;
    std::thread t(worker, GetCurrentThreadId());
 
    MSG msg;
    for (;;)
    {
      int ret = GetMessage(&msg, nullptr, 0, 0);
      if (ret == 0 || ret == -1)
        break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
 
    t.join();
    g1.Revoke();
  }
  CoUninitialize();
  return 0;
}

Hoge::ReadにVisual Studioでブレークポイントを貼って待ち構えます。

その状態で、まずはメインスレッド側の呼び出し履歴を表示します。DispatchMessageから最終的にHoge::Readに到達しています。

メインスレッドの呼び出し履歴(その1)

次に関数workerのスレッドも見てみましょう。こちらはcombase.dll!CCliModalLoop::BlockFnを通りMsgWaitForMultipleObjectsExで待機していることが分かります。

ワーカースレッドの呼び出し履歴(その1)

メソッド呼び出し中にメソッド呼び出し

その状態からさらに2つ目のSTAに対してメソッドを呼び出してみましょう。お手軽に、先のコードのRead関数を改造します。

ATL::CComGITPtr<ISequentialStream> g1; // 1つ目のSTAのオブジェクト
ATL::CComGITPtr<ISequentialStream> g2; // 2つ目のSTAのオブジェクト
 
// ……
 
// Hoge内
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g2.CopyTo(&s));
 
    // 2つ目のSTAのオブジェクトのメソッドを呼び出す。
    char buf;
    s->Write(&buf, sizeof buf, nullptr);
 
    return E_NOTIMPL;
  }
 
// ……
 
void worker(DWORD mainThreadId)
{
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return;
 
  try
  {
    // 2つ目のSTA上でオブジェクトを作成
    ATL::CComObjectStackEx<Hoge> obj;
    g2 = &obj;
 
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g1.CopyTo(&s));
 
    char buf;
    s->Read(&buf, sizeof buf, nullptr);
 
    g2.Revoke();
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
 
  PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
}

今度はHoge::Writeにブレークポイントを貼って止めます。worker関数(2つ目のSTA)→Hoge::Read関数(1つ目のSTA)→Hoge::Write関数(2つ目のSTA)という流れです。

こちらもメインスレッド側の呼び出し履歴を見ます。Hoge::ReadからMsgWaitForMultipleObjectsExに伸びていますね。

main-thread-2

そして、次が関数workerのスレッドです。先ほどもあったcombase.dll!CCliModalLoop::BlockFnから最終的にHoge::Writeに辿り着いています。

worker-thread-2

STAでは、このように外へのメソッド呼び出しの最中に外からのメソッド呼び出しを受け付けることがあります。これが再入 (Re-entrancy)です。

余談:メッセージフィルタ

なお、STAではメソッド・ウィンドウメッセージの受け付けをIMessageFilterCoRegisterMessageFilterである程度制御できます。

終わりに

STAはメッセージループの元で動作するシングルスレッドのアパートメントです。今回は呼び出し履歴(コールスタック)で簡易的にそのことを見てみました。

再入が起こることを忘れていると、予期せぬところでメンバ変数の値が書き換わっているように見えるという一見不思議なバグに遭遇することがあります(ありました)。もちろん、再入で呼び出されたメンバ関数で書き換えられていたのが実態です。STA固有の問題ではありませんが、忘れているとハマることがあるので気をつけましょう。


スポンサード リンク

この記事のカテゴリ

  • ⇒ STAのメソッド呼び出しを見てみる