Create  Edit  Diff  FrontPage  Index  Search  Changes  Login

COMとメモリーリーク

この文章は、ASCIIの.NET入門に書いた内容に似ていますが、完全な書き下ろしです。

2003年にもなって、こんな文章を読む必要がある人はあまりいないような気もしますが、現実にゼロではないようなので書いてみることにします。

インターフェイス定義を知る

MFCを利用したオートメーション以外(注)で、COMを利用する場合、引数のメモリー管理の責任を、呼び出し側と呼ばれた側、どちらが負うかを理解しなければなりません。

そのための第1歩は、インターフェイス定義を参照して呼び出し先のメソッドシグネチャを確認することです。これには、SDK付属のIDLを読む、OLEViewerを利用して型ライブラリ上の定義を照会するといった方法が利用できます。もし、これらの手段によってインターフェイス定義が参照できない場合、OLEオートメーション以外のCOMの呼び出しについてはできないものと考えたほうが無難です。

*
*IDLの例と簡単な読み方の説明
*

次に着目すべき点は、メソッド定義のうち、パラメータの属性です。

たとえば、

HRESULT Method([in]BSTR a, [out]BSTR* b, [in,out]BSTR* c);

であれば、着目すべきは、in/outいった方向指定子となります。

それぞれの意味は

  • in: 呼び出し側がメモリー確保、呼び出し側がメモリー解放
  • out: 呼び出し先でメモリー確保、呼び出し側がメモリー解放
  • in,out: 呼び出し側がメモリー確保、呼び出し先でメモリー解放、呼び出し先でメモリー確保、呼び出し側がメモリー解放

です。また、[out]、[in,out]といったポインタを使用する呼び出しでは、原則的に呼び出し側がNULLを引数として与えることはできません。

……省略……

スマートポインタを利用する

CComBSTRのようなヘルパ(スマートポインタ)を利用すると多くの場合、処理を単純化できます。

HRESULT deleg(LPOLESTR s)
{
  CComBSTR in = s;
  HRESULT h = foo->bar(in);
  return h;
}

この処理をBSTR(BSTR)を利用して記述すると

HRESULT deleg(LPOLESTR s)
{
  BSTR in = SysAllocString(s);
  HRESULT h = foo->bar(in);
  SysFreeString(in);
  return h;
}

のように明示的な解放処理が必要になります。 なお、CComBSTRを利用するのであれば、さらに簡略化して

inline HRESULT deleg(LPOLESTR s)
{
  return foo->bar(CComBSTR(s));
}

と記述することさえ可能です。もちろん、呼び出し側にメモリー解放の責任があるため、

inline HRESULT deleg(LPOLESTR s)
{
  return foo->bar(SysAllocString(s)); // SysFreeStringが呼び出されないためメモリーリーク
}

と記述することはできません。

CComBSTRのようなスマートポインタを利用する場合、注意点がひとつあります。それは、1度使用すると決定したら、必ずスマートポインタが提供するメソッドを利用しなければならないということです。

特に、ATLの場合、ATLのソースを参照すると頻繁にインターフェイスのスマートポインタ(CComPtr)のデータメンバ(p)を直接使用する個所が出てくるため、CComBSTRについてもそのような呼び出しが許されるかのように考えてしまうかも知れません。

HRESULT deleg(LPOLESTR s)
{
  CComBSTR in;
  in.m_str = SysAllocString(s); // 直接データメンバへ代入
  HRESULT h = foo->bar(in);
  return h;
}

上の例は正しく実行されます。しかし、

HRESULT deleg(LPOLESTR s)
{
  CComBSTR in;
  in.m_str = SysAllocString(s); // 直接データメンバへ代入
  HRESULT h = foo->bar(in);
  if (h != S_OK)
  {
    WCHAR sz[in.Length() + 8];
    wcscpy(sz, L"MAGIC:");
    wcscat(sz, s);
    in.m_str = SysAllocString(sz); // 直接データメンバへ代入
    h = foo->bar(in);
  }
  return h;
}

この場合、最初にアロケーションしたsの分の解放が行われません。もちろん、2度目の代入の前に直接<code>SysFreeString?(in.m_str);</code>と呼び出すことは可能ですが、この呼び出しはデストラクタでの2重解放の原因となりかねないため、絶対に行ってはなりません。

正しくは、CComBSTRのオペレータ=を呼び出すことです。

HRESULT deleg(LPOLESTR s)
{
  CComBSTR in = s;
  HRESULT h = foo->bar(in);
  if (h != S_OK)
  {
    WCHAR sz[in.Length() + 8];
    wcscpy(sz, L"MAGIC:");
    wcscat(sz, s);
    in = sz;
    h = foo->bar(in);
  }
  return h;
}

上の書き方であれば、オペレータ=の呼び出しで現在保持しているBSTRが解放されるため、メモリーリークは起きません。

なお、以下のようにoutin,outで利用する場合には、直接データメンバm_strを記述しても構いません。

CComBSTR inout = "input";
HREUSLT h = foo->bar(&inout.m_str);
if (h == S_OK && wcscmp(inout, "output") == 0)
{
   ...
}

しかし、CComBSTRはオペレータ&をオーバーロードしているため、次のように記述したほうが良いでしょう。

CComBSTR inout = "input";
HRESULT h = foo->bar(&inout);
...

CComBSTR::Copyは、呼ばれた側がoutin,out引数へ文字列を与える場合に利用します。

HRESULT STDMETHODCALLTYPE Foo::bar(BSTR* inout)
{
   CComBSTR result = inout;
   ...
   SysFreeString(inout);     // 割り当て前に解放する
   *inout = result.Copy();
   return S_OK;
}

なぜなら、もしCopyを利用して新たなBSTRのインスタンスを生成しないで

HRESULT STDMETHODCALLTYPE Foo::bar(BSTR* inout)
{
   CComBSTR result = inout;
   ...
   SysFreeString(inout);     // 割り当て前に解放する
   *inout = BSTR(result);
   return S_OK;
}

と記述すると、このメソッドを退出した時点でresultのデストラクタが呼び出され、result.m_strが指すBSTRのインスタンス(それは*inoutへ設定したインスタンスと同一です)が解放されるからです。その場合、呼び出し側は解放済みのBSTRを出力として参照することになるため、運不運に依存した結果となってしまいます。

(続くかも)

Last modified:2003/10/17 16:04:08
Keyword(s):
References: