2013-06-21 43 views
7

我有一個用C#編寫的託管COM對象和一個用C++(MFC和ATL)編寫的本地COM客戶端和接收器。客戶端創建該對象並在啓動時向其事件接口提供建議,並從其事件接口取消並在關閉時釋放該對象。問題在於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(); 
} 
+0

在我看來,你忘記了m_TestObjectEventsSink-> Release()。它不是自動的,因爲你存儲了一個指向CComObject <>的指針,你可能只是在泄漏它。不知道爲什麼這是必要的。 –

+0

哎呀,對不起。忘了這些,但效果是一樣的CComObject :: CreateInstance()爲您提供ref對象爲0.我會更新問題,無論。 – Luke

+0

CComObject :: CreateInstance()爲您提供ref對象爲0的對象;這是你的責任AddRef()它。 – Luke

回答

3

首先,我只是說我用過你的示例代碼來實現您所描述的內容的副本,但在測試Debug或Release版本時,我看不到任何訪問衝突。

因此,您可能會看到一些替代解釋(例如,如果您持有本地客戶端的其他接口,您可能需要撥打Marshal.ReleaseCOMObject)。

有一個關於MSDN here何時/何時不致電ReleaseCOMObject的綜合描述。

說了這麼多,你是對的,你的C#的COM對象不與COM客戶端的接收器對象直接工作,但它確實通過 C#的事件對象與它通信。這允許您實施自定義事件對象,以便您可以將客戶的呼叫的效果陷入AtlAdviseAtlUnadvise

例如,你可以重新實現您的活動如下(添加了一些調試輸出):

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")); 
    } 
} 

這給出了以下調試輸出(你可以看到C#跟蹤從AtlAdvise調用在那裏)

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)

這看起來和預期的一樣,我們的引用計數爲2 - 從本機c ode AddRef和另一個(推測)來自AtlAdvise。現在

,您可以檢查會發生什麼,如果InvokeTestEvent()方法被調用 - 我在這裏做兩次:

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不影響引用計數(also noted by other people),但還請注意,我們從C#COM ob的remove訪問器ject事件,這是強制執行一些垃圾回收或其他拆卸任務的可能位置。

總結:

  1. 您報道的訪問衝突與您發佈的代碼,但我無法重現該錯誤,所以可能你看到的錯誤是無關的,你所描述的問題。
  2. 您問過如何與COM客戶端接收器進行交互,並且我已經展示了一種使用自定義事件實現的潛在方式。調試輸出支持這兩個COM組件如何交互。

我真的希望這是有幫助的。在this old but otherwise excellent blog post有一些替代COM處理技巧和更多解釋。

+0

我不確定該博文是否相關。它正在消耗託管代碼中的COM對象,並將其定製爲確定性釋放。在我的情況下,COM對象本身被管理並在本機代碼中被使用。在託管代碼中,我從來沒有對事件接收器的引用,因此我無法在其上調用ReleaseComObject()。我會在event.remove()期間強調GC,看看它是否做了什麼。 – Luke

+0

在event.remove()期間強制GC不會減少ref計數。 – Luke

+0

@Luke是的,對不起,博客文章是在那裏,因爲我發現它在過去很有用,但你說得對,對你來說是相反的情況。另外,我也嘗試過GC.Collect,但我認爲COM包裝仍然保持打開狀態,直到對象本身消失。我主要關心的是缺少訪問違規,所以我無法繼續嘗試調試原因。我認爲,通過捕獲AtlUnadvise,您可能可以在那裏爲該問題申請清除。 –

相關問題