本記事は、COM Advent Calendar 2014 – Qiitaの21日目の記事です。まだネタは尽きていないのに、日数の残りが少なくて、少し戸惑っています。


今回は、OBJREFモニカーの寿命について調べてみました。すると結局、タイトルにあるようにスタブオブジェクトの寿命であることに気付いたという話です。

サンプルコードは前回からの改変で話を進めますが、そんなわけで、OBJREFモニカーだけでなく、特に後半はアパートメントをまたぐ場合すべてに当てはまる話です。

OBJREFモニカーの寿命

OBJREFモニカーおよびその文字列が有効な期間は、OBJREFが参照するスタブオブジェクトの寿命と同じです。つまり、他アパートメントの側でReleaseして参照カウントが0になったら終わってしまう、というのがデフォルトの挙動です。

まずはその挙動を確かめてみます。前回のサンプルコードのVBScript側を以下のように2回GetObjectするように変えます。途中でNothingを代入して参照を消しているのがミソです。

objref = WScript.Arguments.Named("str")
WScript.Echo "--------"
WScript.Echo "objref.vbs: " & objref
 
WScript.Echo "--------"
Set hoge = GetObject(objref)
hoge()
Set hoge = Nothing
 
WScript.Echo "--------"
Set hoge = GetObject(objref)
hoge()

すると実行結果はこうなります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
T:\objref.vbs(9, 1) (null): オブジェクトは登録されていません

真ん中のSet hoge = Nothingの行で、スタブオブジェクトが用済みとなったため捨てられてしまい、OBJREFも無効になります。そのため、2回目のGetObjectは失敗に終わります。大本のオブジェクト自体の参照カウントがたとえまだ0でなかったとしても、結果は同じです。

そのため、試しに真ん中のSet hgoe = Nothingの行を無くすと、以下のように2回目のGetObjectも成功することが分かります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
Hoge::Invoke

スタブを生かし続ける

上記の挙動では困る、すなわち1度ならず何度でもOBJREFモニカーを使えるようにしたい場合もあります。

その方法は2つあります。1つはIExternalConnectionを実装する方法、もう1つはCoLockObjectExternal関数を利用する方法です。今回はCoLockObjectExternal関数を使ってみます。

前回のC++側コードを少し変更し、CreateObjrefMoniker関数呼び出しの付近にCoLockObjectExternal関数を追加します。CreateObjrefMonikerとの順番はどちらが先でも構いません。

IDispatchPtr hoge;
ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
IMonikerPtr moniker;
ATLENSURE_SUCCEEDED(CreateObjrefMoniker(hoge, &moniker));
ATLENSURE_SUCCEEDED(CoLockObjectExternal(hoge, TRUE, FALSE));

VBScript側のSet hoge = Nothingの行を元に戻して実行した結果です。以下のように2回目のGetObjectも成功します。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
Hoge::Invoke

スタブが捨てられて、また作られることの確認

先ほど「スタブオブジェクトが捨てられる」と書きました。それを確認してみます。もう1度OBJREFモニカーを作り、それが別の文字列になっていることでもって確かめられます。

前回のコードのmain関数部分を以下のコードに置き換えます。OBJREFモニカーの文字列を作る処理を別の関数に切り出しました。

std::wstring GetObjrefMonikerDisplayString(IUnknown* obj)
{
  IMonikerPtr moniker;
  ATLENSURE_SUCCEEDED(CreateObjrefMoniker(obj, &moniker));
  IBindCtxPtr bc;
  ATLENSURE_SUCCEEDED(CreateBindCtx(0, &bc));
  LPOLESTR monikerStr;
  ATLENSURE_SUCCEEDED(moniker->GetDisplayName(bc, nullptr, &monikerStr));
  BOOST_SCOPE_EXIT_ALL(&monikerStr) { CoTaskMemFree(monikerStr); };
  return monikerStr;
}
 
int main()
{
  try
  {
    ATLENSURE_SUCCEEDED(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    IDispatchPtr hoge;
    ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
 
    std::wstring monikerStr1 = GetObjrefMonikerDisplayString(hoge);
    ATLENSURE_SUCCEEDED(CoLockObjectExternal(hoge, TRUE, FALSE));
 
    WCHAR sysDir[MAX_PATH] = {};
    GetSystemDirectoryW(sysDir, ARRAYSIZE(sysDir));
    std::wostringstream ss;
    ss << sysDir << L"\\cscript.exe //nologo objref.vbs /str:" << monikerStr1;
    std::wcout << L"main: " << ss.str() << std::endl;
    _wsystem(ss.str().c_str());
 
    std::wstring monikerStr2 = GetObjrefMonikerDisplayString(hoge);
    std::wcout << L"main 2: " << monikerStr2 << std::endl;
    std::wcout << L"--------" << std::endl;
    std::wcout << std::boolalpha << (monikerStr1 == monikerStr2) << std::endl;
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << std::hex << std::showbase;
    std::clog << e.m_hr << ' ' << ATL::AtlGetErrorDescription(e) << std::endl;
  }
 
  CoUninitialize();
}

OBJREFモニカーを作る→VBScript側でGetObjectしてスタブを破棄→OBJREFモニカーを作る、という順で実行されます。最後に2つのOBJREFモニカーの文字列を比較した結果を出力させています。

実行すると次のようになります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
T:\objref.vbs(11, 1) (null): オブジェクトは登録されていません

main 2: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
false

最後の出力(monikerStr1 == monikerStr2)がfalseとなっています。

このプログラムも、最初のGetObjrefMonikerDisplayString (CreateObjrefMoniker)関数呼び出しの前後にCoLockObjectExternal関数呼び出しを追加した場合を試します。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
Hoge::Invoke
main 2: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
true

見事、trueになります。同じオブジェクトに対するスタブが存在するため、2度目のCreateObjrefMonikerでも同じOBJREFが使用されます。そのため、GetDisplayNameでも同じ文字列が得られました。

おまけ:IExternalConnection

今回IExternalConnectionは取り上げませんでした。そのため、IExternalConnectionを扱っている日本語ウェブページのリンクを並べておきます。私も、幾度となく参照しました。

取り上げなかった理由を強いて挙げるとすれば、私のIExternalConnectionを使う理由が外部からの切断(最終Release)の検知であることが多いからかなと思います。

ロックしたオブジェクトの寿命を考える

CoLockObjectExternalやIExternalConnectionでロックすると、他アパートメントからいくらReleaseしてもスタブオブジェクトから大元のオブジェクトへの参照を保持し続けます。オブジェクトの参照カウントはいったいいつ0になって、いつ削除されるのでしょう。

答えは、「自ら決める」です。たとえば、GUIアプリケーションで主たるウィンドウが閉じられたり、Windowsサービスアプリケーションで終了が要求されたりしたらオブジェクトも利用不可能にする方式が考えられます。他アパートメントの利用状況とは一切関係なく、自らの都合で切断するのです

具体的には、CoUninitialize()でアパートメントごと終了させたり、CoDisconnectObject関数で個々のオブジェクトごとに切断を言い渡したりできます。また、CoLockObjectExternal関数の2番目の引数にFALSEを渡すことで、ロックを解除する(スタブは必要に応じて生き残るかもしれない)方法もあります。

まとめ

OBJREFモニカーはOBJREFに連動して、プロキシオブジェクト(他アパートメント)からの参照次第で寿命が決まります。それが困るならCoLockObjectExternalまたはIExternalConnectionでスタブオブジェクトをロックできます。自オブジェクトあるいは自アプリの都合で寿命を決めたい場合にロックは有用です。

マーシャリングのことなのでプロセス間・プロセス内関係なく当てはまる話ですが、プロセス間でないと遭遇しにくい問題という印象があります。やはり、同一プロセス内、CoMarshalInterThreadInterfaceInStreamやGlobal Interface Tableを使っている限り、スタブの寿命を気にする事態に直面することはなかなかありません。


スポンサード リンク

この記事のカテゴリ

  • ⇒ スタブオブジェクトの寿命とそのロック