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


QueryInterfaceで増えるオブジェクト (2)の続きです。

初回(QueryInterfaceで増えるオブジェクト)で以下のことが分かりました。

  • IStreamをQueryInterfaceするたびに異なるポインタが得られる。
  • 同じオブジェクトから得たIStreamから、IUnknownをQueryInterfaceすると同一のポインタが得られる。

ここから、迂闊にIUnknownを介すると罠にはまるだろうと考えられます。それを以下のプログラムで試してみました。

#include <iostream>
#include <windows.h>
#include <comdef.h>
#include <atlbase.h>
#include <atlutil.h>
 
#import <msxml6.dll>
 
void f(_In_ IStream* s)
{
  char buf[6];
  ULONG read;
 
  ATLENSURE_SUCCEEDED(s->Read(buf, sizeof buf, &read));
  std::cout << "read #1: ";
  std::cout.write(buf, read) << std::endl;
 
  IUnknownPtr u = s; // QueryInterface
 
  IStreamPtr ss = u; // QueryInterface
 
  ATLENSURE_SUCCEEDED(ss->Read(buf, sizeof buf, &read));
  std::cout << "read #2: ";
  std::cout.write(buf, read) << std::endl;
}
 
int main()
{
  CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE);
  try
  {
#if 1
    MSXML2::IXMLDOMDocumentPtr doc(__uuidof(MSXML2::DOMDocument60));
    if (!doc->loadXML(L"<hoge><piyo/></hoge>"))
    {
      std::clog << "Bad XML!" << std::endl;
      return 1;
    }
 
    IStreamPtr s1 = doc;
#else
    const char* str = "<hoge><piyo/></hoge>";
    IStreamPtr s1(SHCreateMemStream(reinterpret_cast<const BYTE*>(str), std::strlen(str)), false);
    if (s1 == nullptr)
      throw _com_error(E_OUTOFMEMORY);
#endif
    f(s1);
  }
  catch (const _com_error& e)
  {
    std::clog << e.ErrorMessage() << std::endl;
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
}

関数fをご覧ください。まず、仮引数のIStreamでReadを呼び出しています。その後、わざとIUnkonwnへQueryInterfaceし、そこからIStreamへ再びQueryInterfaceしています。そしてそちらのIStreamで2回目のReadを実行しています。

#if 1でDOMDocumentのIStreamを渡すとこうなります。2回目のRead呼び出しでも、頭からの読み出しになってしまいました。IUnknownからIStreamをQueryInterfaceしている処理が新しいIStreamを取得することになっているためと説明できます。

read #1: <hoge>
read #2: <hoge>

比較として、#if 0に書き換えて、SHCreateMemStreamによるIStreamを渡した場合の結果がこれです。こちらは何度IStreamをQueryInterfaceしても同じであるため、続きが読み出せています。こちらが期待した結果です。

read #1: <hoge>
read #2: <piyo/

まとめです。

MSXMLのDOMDocumentオブジェクトでは、IStreamをQueryInterfaceするたびに新しい従属的なオブジェクトを返します。そのため、そこからIUnknownにQueryInterfaceし、さらにIStreamをQueryInterfaceすると、同じポインタが得られません。

このため、「IStreamからの読み出し処理をIUnknownで受け渡して複数の関数で実行する」という処理の場合、DOMDocumentのIStreamを渡すと期待どおり動きません。IXMLDOMDocument.saveで適当なIStreamに書き出してそちらを使うべきということになります。

QueryInterfaceで毎回異なるポインタを返す(新しく従属的なオブジェクトを作って返す)こと自体は有用な場合もあるとは思います。しかし、このような罠が生じるので、状態を持つ内部オブジェクトを作って返すのは好ましくないと私は感じました。


スポンサード リンク

この記事のカテゴリ

  • ⇒ QueryInterfaceで増えるオブジェクト (3)