7

C# で記述されたマネージ COM オブジェクトと、C++ (MFC および ATL) で記述されたネイティブ COM クライアントとシンクがあります。クライアントはオブジェクトを作成し、起動時にそのイベント インターフェイスに通知し、イベント インターフェイスから通知を取り消し、シャットダウン時にオブジェクトを解放します。問題は、COM オブジェクトが、ガベージ コレクションが実行されるまで解放されないシンクへの参照を持っていることです。ガベージ コレクションが実行されると、クライアントは既に破棄されているため、通常はアクセス違反が発生します。とにかくクライアントがシャットダウンしているので、おそらくそれほど大したことではありませんが、可能であればこれを適切に解決したいと考えています. シンク オブジェクトをよりタイムリーに解放するには、COM オブジェクトが必要ですが、COM オブジェクトがシンク オブジェクトと明示的に連携しないため、どこから始めればよいかわかりません。

私のCOMオブジェクト:

public delegate void TestEventDelegate(int i);

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITestObject
{
    int TestMethod();
    void InvokeTestEvent();
}

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITestObjectEvents
{
    void TestEvent(int i);
}

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITestObjectEvents))]
public class TestObject : ITestObject
{
    public event TestEventDelegate TestEvent;
    public TestObject() { }
    public int TestMethod()
    {
        return 42;
    }
    public void InvokeTestEvent()
    {
        if (TestEvent != null)
        {
            TestEvent(42);
        }
    }
}

クライアントは標準の MFC ダイアログ ベースのプログラムで、ATL のサポートが追加されています。私のシンククラス:

class CTestObjectEventsSink : public CComObjectRootEx<CComSingleThreadModel>, public ITestObjectEvents
{
public:
    BEGIN_COM_MAP(CTestObjectEventsSink)
        COM_INTERFACE_ENTRY_IID(__uuidof(ITestObjectEvents), ITestObjectEvents)
    END_COM_MAP()
    HRESULT __stdcall raw_TestEvent(long i)
    {
        return S_OK;
    }
};

ダイアログクラスには次のメンバーがあります。

ITestObjectPtr m_TestObject;
CComObject<CTestObjectEventsSink>* m_TestObjectEventsSink;
DWORD m_Cookie;

OnInitDialog() で:

HRESULT hr = m_TestObject.CreateInstance(__uuidof(TestObject));
if(m_TestObject)
{
    hr = CComObject<CTestObjectEventsSink>::CreateInstance(&m_TestObjectEventsSink);
    if(SUCCEEDED(hr))
    {
        m_TestObjectEventsSink->AddRef(); // CComObject::CreateInstace() gives an object with a ref count of 0
        hr = AtlAdvise(m_TestObject, m_TestObjectEventsSink, __uuidof(ITestObjectEvents), &m_Cookie);
    }
}

OnDestroy() で:

if(m_TestObject)
{
    HRESULT hr = AtlUnadvise(m_TestObject, __uuidof(ITestObjectEvents), m_Cookie);
    m_Cookie = 0;
    m_TestObjectEventsSink->Release();
    m_TestObjectEventsSink = NULL;
    m_TestObject.Release();
}
4

1 に答える 1

3

まず、サンプル コードを使用して、説明した内容のコピーを実装しましたが、デバッグ ビルドまたはリリース ビルドをテストするときにアクセス違反は見られません。

そのため、表示されているものに別の説明がある可能性があります (たとえばMarshal.ReleaseCOMObject、ネイティブ クライアントへの他のインターフェイスを保持している場合は、呼び出す必要がある場合があります)。

MSDN hereでいつ/いつ電話ReleaseCOMObjectしないかについての包括的な説明があります。

そうは言っても、C# COM オブジェクトが COM クライアントのシンク オブジェクトと直接連携しないことは間違いありませんが、C# イベント オブジェクトを介して通信します。これにより、カスタマイズされたイベント オブジェクトを実装できるため、クライアントによるAtlAdviseおよびへの呼び出しの影響をトラップできますAtlUnadvise

たとえば、次のようにイベントを再実装できます (デバッグ出力を追加します)。

private event TestEventDelegate _TestEvent;
public event TestEventDelegate TestEvent
{
    add
    {
        Debug.WriteLine("TRACE : TestObject.TestEventDelegate.add() called");
        _TestEvent += value;
    }
    remove
    {
        Debug.WriteLine("TRACE : TestObject.TestEventDelegate.remove() called");
        _TestEvent -= value;
    }
}

public void InvokeTestEvent()
{
    if (_TestEvent != null)
    {
        _TestEvent(42);
    }
}

デバッグ出力を続行するには、MFC/ATL アプリケーションに同様の診断を追加し、シンク インターフェイスで参照カウントが更新されるタイミングを正確に確認します (これは、両方のプロジェクトのデバッグ ビルドを想定していることに注意してください)。たとえば、Dumpシンクの実装にメソッドを追加しました。

class CTestObjectEventsSink : public CComObjectRootEx<CComSingleThreadModel>, public ITestObjectEvents
{
public:
    BEGIN_COM_MAP(CTestObjectEventsSink)
        COM_INTERFACE_ENTRY_IID(__uuidof(ITestObjectEvents), ITestObjectEvents)
    END_COM_MAP()
    HRESULT __stdcall raw_TestEvent(long i)
    {
        return S_OK;
    }
    void Dump(LPCTSTR szMsg)
    {
        TRACE("TRACE : CTestObjectEventsSink::Dump() - m_dwRef = %u (%S)\n", m_dwRef, szMsg);
    }
};

次に、IDE を使用してデバッグ クライアント アプリケーションを実行すると、何が起こっているかを確認できます。まず、COM オブジェクトの作成中:

HRESULT hr = m_TestObject.CreateInstance(__uuidof(TestObject));
if(m_TestObject)
{
    hr = CComObject<CTestObjectEventsSink>::CreateInstance(&m_TestObjectEventsSink);
    if(SUCCEEDED(hr))
    {
        m_TestObjectEventsSink->Dump(_T("after CreateInstance"));
        m_TestObjectEventsSink->AddRef(); // CComObject::CreateInstace() gives an object with a ref count of 0
        m_TestObjectEventsSink->Dump(_T("after AddRef"));
        hr = AtlAdvise(m_TestObject, m_TestObjectEventsSink, __uuidof(ITestObjectEvents), &m_Cookie);
        m_TestObjectEventsSink->Dump(_T("after AtlAdvise"));
    }
}

AtlAdviseこれにより、次のデバッグ出力が得られます (呼び出しからの C# トレースを確認できます)。

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 0 (after CreateInstance)
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 1 (after AddRef)
TRACE : TestObject.TestEventDelegate.add() called
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 2 (after AtlAdvise)

AddRef1 つはネイティブ コードから、もう 1 つは (おそらく) から2 の参照カウントがありますAtlAdvise

InvokeTestEvent()これで、メソッドが呼び出された場合に何が起こるかを確認できます- ここでは 2 回行います。

m_TestObject->InvokeTestEvent();
m_TestObjectEventsSink->Dump(_T("after m_TestObject->InvokeTestEvent() first call"));
m_TestObject->InvokeTestEvent();
m_TestObjectEventsSink->Dump(_T("after m_TestObject->InvokeTestEvent() second call"));

これは対応するトレースです

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() first call)   
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() second call) 

AddRefイベントが最初に発生したときに追加が発生したことがわかります。これは、ガベージ コレクションまで解放されない参照であると推測しています。

最後に、OnDestroy参照カウントが再び減少していることがわかります。コードは

if(m_TestObject)
{
    m_TestObjectEventsSink->Dump(_T("before AtlUnadvise"));
    HRESULT hr = AtlUnadvise(m_TestObject, __uuidof(ITestObjectEvents), m_Cookie);
    m_TestObjectEventsSink->Dump(_T("after AtlUnadvise"));
    m_Cookie = 0;
    m_TestObjectEventsSink->Release();
    m_TestObjectEventsSink->Dump(_T("after Release"));
    m_TestObjectEventsSink = NULL;
    m_TestObject.Release();
}

トレース出力は

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (before AtlUnadvise)
TRACE : TestObject.TestEventDelegate.remove() called
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after AtlUnadvise)
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 2 (after Release)

AtlUnadviseしたがって、参照カウントには影響しないことがわかりますが(他の人も指摘しています)、C# COM オブジェクト イベントのアクセサーからトレースを取得したことにも注意してくださいremove。これは、ガベージ コレクションまたはその他のティアを強制する可能性のある場所です-ダウンタスク。

要約すると:

  1. 投稿されたコードでアクセス違反が報告されましたが、そのエラーを再現できなかったため、表示されたエラーは、あなたが説明した問題とは無関係である可能性があります。
  2. COM クライアント シンクと対話する方法を尋ねられたので、カスタマイズされたイベント実装を使用する 1 つの潜在的な方法を示しました。これは、2 つの COM コンポーネントがどのように相互作用するかを示すデバッグ出力でサポートされています。

これがお役に立てば幸いです。この古いながらも優れたブログ投稿には、代替の COM 処理のヒントと詳細な説明がいくつかあります。

于 2013-07-01T14:58:41.697 に答える