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


COMインタフェースを実装するクラスを書いていると、どのアパートメントからでもマーシャリングせず直に呼び出しを受け付けられるものになることがあります。あるいは、マーシャリングが邪魔だと思うことがあります。COMには、そんなオブジェクトを作成する手段が用意されています。

このようなオブジェクトの概念はフリースレッドマーシャラー (FTM)として以前から存在していましたが、Windows Runtime APIでこの概念にAgile Objectと名前が与えられました。最近では、Accessing Interfaces Across Apartmentsなど、MSDNライブラリの従来のCOM関係の記事でもこの言葉が使われているようです。

サンプルコード

その方法は2つあり、従来用意されてきたフリースレッドマーシャラー (FTM)の集成 (Aggregation)と、Windows 8で新しく追加されたIAgileObjectです。両者ともに実装するのが良いでしょう。

#include <atomic>
#include <iostream>
#include <thread>
#include <windows.h>
#include <comdef.h>
 
MIDL_INTERFACE("c4230d66-43bb-42a9-9f70-f580f5b96866") IHoge : IUnknown
{
  STDMETHOD(Run)();
};
 
_COM_SMARTPTR_TYPEDEF(IHoge, __uuidof (IHoge));
 
class Hoge : public IHoge
{
  Hoge()
  {
    auto hr = CoCreateFreeThreadedMarshaler(this, &m_marshaler);
    if (FAILED(hr))
      throw _com_error(hr);
  }
 
public:
  static IHogePtr Create() { return new Hoge; }
 
  IFACEMETHOD(QueryInterface)(REFIID riid, void** ppv) override
  {
    if (ppv == nullptr)
    {
      return E_POINTER;
    }
    // IAgileObjectは追加のメソッドを持たないので
    // IUnknownと同じ値を返せば良い。
    if (riid == __uuidof (IHoge)
      || riid == __uuidof (IUnknown)
      || riid == __uuidof (IAgileObject))
    {
      *ppv = static_cast<IHoge*>(this);
      AddRef();
      return S_OK;
    }
    else if (riid == __uuidof (IMarshal))
    {
      // フリースレッドマーシャラに丸投げ。
      return m_marshaler->QueryInterface(riid, ppv);
    }
    *ppv = nullptr;
    return E_NOINTERFACE;
  }
 
  IFACEMETHOD_(ULONG, AddRef)() override
  {
    return ++m_count;
  }
 
  IFACEMETHOD_(ULONG, Release)() override
  {
    auto count = --m_count;
    if (count == 0)
    {
      delete this;
    }
    return count;
  }
 
  IFACEMETHOD(Run)()
  {
    std::cout << "Run: " << std::this_thread::get_id() << std::endl;
    return S_OK;
  }
 
private:
  IUnknownPtr m_marshaler;
  std::atomic<ULONG> m_count{};
};
 
void worker(_In_ IStream* s)
{
  try
  {
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
 
    IHogePtr hoge;
    _com_util::CheckError(CoGetInterfaceAndReleaseStream(
      s, IID_PPV_ARGS(&hoge)));
 
    std::cout << "hoge in worker: " << hoge.GetInterfacePtr() << std::endl;
    std::cout << "worker thread: " << std::this_thread::get_id() << std::endl;
 
    hoge->Run();
  }
  catch(const _com_error& e)
  {
    std::cout << std::hex << std::showbase << e.Error() << std::endl;
  }
  CoUninitialize();
}
 
int main()
{
  try
  {
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    auto hoge = Hoge::Create();
    std::cout << "hoge in main: " << hoge.GetInterfacePtr() << std::endl;
    IStream* s;
    _com_util::CheckError(
      CoMarshalInterThreadInterfaceInStream(__uuidof (IUnknown), hoge, &s));
    std::thread(worker, s).join();
  }
  catch(const _com_error& e)
  {
    std::cout << std::hex << std::showbase << e.Error() << std::endl;
  }
 
  CoUninitialize();
}

フリースレッドマーシャラの作成にはCoCreateFreeThreadedMarshaler関数を使います。この実引数には、CoCreateInstandeの2番目の実引数と同じく外側のオブジェクトへのポインタを渡します。

QueryInterfaceでは、この2つを行います。

  • IAgileObjectの要求に対して自身のオブジェクトを返します。
  • IMarshalの要求に対してフリースレッドマーシャラを返します。

IAgileObjectはメソッドを持たないインタフェースです。そのため、INoMarshalの場合(参考:前々回のマーシャル禁止のオブジェクト)と同じように、C++基底クラスに入れずに実装しています。

実行結果は以下のようになります。

hoge in main: 011D8720
hoge in worker: 011D8720
worker thread: 11092
Run: 11092

この結果では、以下のことが示されています。

  • マーシャリングに対応させていないインタフェースIHogeでも別アパートメントに持っていけている(マーシャリングできない場合の動きで失敗したのとは対照的に)
  • ポインタの値がアパートメントをまたいでも元と同じ
  • Runメソッドが呼び出し元のworkerスレッドで実行されている

なお、その性質から、今回のようにAgile Objectだと分かっているならわざわざCoGetInterfaceAndReleaseStreamなどでマーシャリングする必要はありません。直接ポインタをやりとりして平気です。

Agile Object実装時の注意

マーシャリングのことを考えなくて済むのが便利なAgile Objectですが、そのことに起因する注意点もあります。

メソッドが実行されるアパートメントが毎回異なる可能性があるため、C++などの言語でクラスとして実装する場合、メンバー変数にCOMインタフェースへのポインタを持つには注意が必要です。

  • 基本的には、適切にマーシャリングする必要があります。ATL::CComGITPtrPlatform::Agile (C++/CX)などのマーシャリング処理をラップしたクラステンプレートが便利です。
  • 例外として、ポインタの指すオブジェクトがAgile Objectであればそのままポインタを保持して問題ありません。

従来のCOMでは、マーシャリングが必要なオブジェクトの多いので前者の注意点が目立ちます。ところが、Windows Runtime APIでは後述するようにAgile Objectが大半であるため、ほとんど気にする必要がなくなってしまいました。

参考:

Windows Runtime APIにおけるAgile Object

今日の記事では、たびたびWindows Runtime API(WinRT API: Windowsストア・Phoneアプリ向けAPI)の言葉が登場しています。というのも、WinRT APIではAgile Objectが第一級に抜擢されたためです。

まず、従来のCOMにおいてFTMはやや特殊な位置づけでしたが、WinRT APIではクラスの実装形態の1つとして明確に位置づけられました。メタデータ (WinMD)上で、クラスに対する属性として表現されます。MarshalingBehaviorAttribute属性に以下のMarshalingType列挙体が使用されます。

  1. None
  2. Agile
  3. Standard

Noneがマーシャリング禁止 (INoMarshal)、Agileがただ今扱っているAgile Object、Standardが従来のCOM同様のマーシャリングとなります。

MSDNライブラリのThreading and Marshaling (C++/CX)によれば、Windows Runtime APIのクラスのうち90%はAgile Objectだそうです。ASTAのUIスレッドが1つのみ、それ以外はすべてMTAであることも併せて考えれば、Windowsストアアプリにおいては概ね以下のように理解すれば十分ということになります。

  • UI関係のオブジェクトがUIスレッドへマーシャリングされる
  • それ以外のオブジェクトの実行スレッドについてCOMおよびWinRT APIは関与しない

このように、Windowsストア・Phoneアプリ作成においては、アパートメントやマーシャリングなどCOMのことをあまり知らなくてもいいようにしようと工夫していることが窺えます。そのため、Agile Objectが第一級と表現したわけです。

終わりに

今回WinRT APIの連呼になりましたが、Agile Object自体は非WinRT APIアプリであるデスクトップアプリでも便利に使えます。

WinRT APIのように、UI関係以外は積極的にAgile Object化して、マーシャリングを考えずに済むようにするのが良いと思います。もちろん、ブロック処理が含まれていればちょっと考え物ですね。Agile Object化をやめてMTAオブジェクトにするか、非同期処理にしてAgile Object化するか、時と場合によります。

以上、利用頻度の低いわけがない(と私は思っている)機能にもかかわらず、インターネット上であまり話を見ないAgile Object(フリースレッドマーシャラー)の話でした。

スポンサード リンク

この記事のカテゴリ

  • ⇒ Agile Objectまたはフリースレッドマーシャラーの集成
  • ⇒ Agile Objectまたはフリースレッドマーシャラーの集成