2013-06-21 2 views
7

Ich habe ein verwaltetes COM-Objekt geschrieben in C# und einen nativen COM-Client und Senke in C++ geschrieben (MFC und ATL). Der Client erstellt das Objekt und informiert seine Ereignisschnittstelle beim Start, entlädt sich von seiner Ereignisschnittstelle und gibt das Objekt beim Herunterfahren frei. Das Problem ist, dass das COM-Objekt einen Verweis auf die Senke hat, die erst freigegeben wird, wenn die Garbage Collection ausgeführt wird. Zu diesem Zeitpunkt ist der Client bereits abgerissen und führt daher normalerweise zu einer Zugriffsverletzung. Es ist wahrscheinlich nicht so eine große Sache, da der Client sowieso heruntergefahren wird, aber ich würde das gerne nach Möglichkeit auflösen. Ich brauche mein COM-Objekt, um mein Sink-Objekt schneller freizugeben, und ich weiß nicht, wo ich anfangen soll, da mein COM-Objekt nicht explizit mit dem Sink-Objekt arbeitet.Wie wird die Lebensdauer von Objekten beim Arbeiten mit COM-Interop verwaltet?

Mein COM-Objekt:

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

Der Client ist ein Standard-MFC-Dialog-Programm, mit zusätzlicher Unterstützung für ATL. Meine Senke-Klasse:

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

Ich habe folgende Mitglieder in meiner Dialogklasse:

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

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

In 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

Sieht mir aus, Sie vergessen m_TestObjectEventsSink-> Release(). Es ist nicht automatisch, da Sie einen Zeiger auf CComObject <> speichern, wahrscheinlich werden Sie es gerade verlieren. Nicht sicher, warum das notwendig wäre. –

+0

Hoppla, tut mir leid. Habe das vergessen, aber der Effekt ist der gleiche wie CComObject :: CreateInstance() gibt dir ein Objekt mit einem ref count von 0. Ich werde die Frage trotzdem aktualisieren. – Luke

+0

CComObject :: CreateInstance() gibt Ihnen ein Objekt mit einem ref count von 0; es liegt in Ihrer Verantwortung, AddRef() es. – Luke

Antwort

3

Zunächst einmal werde ich nur sagen, dass ich Ihre verwendet habe Beispielcode, um eine Kopie von dem zu implementieren, was Sie beschrieben haben, aber ich sehe keine Zugriffsverletzungen beim Testen von Debug- oder Release-Builds.

Es ist also möglich, dass es eine alternative Erklärung zu dem gibt, was Sie sehen (z. B. müssen Sie Marshal.ReleaseCOMObject anrufen, wenn Sie andere Schnittstellen zum nativen Client haben).

Es gibt eine umfassende Beschreibung, wann/wann nicht ReleaseCOMObject unter MSDN here anzurufen ist.

gesagt hat, dass Sie Recht haben, dass Ihr C# COM-Objekt funktioniert nicht mit dem Senke-Objekt COM-Client direkt, aber es tut mit ihm durch die C# -Ereignisobjekts kommunizieren. Dadurch können Sie ein benutzerdefiniertes Ereignisobjekt implementieren, sodass Sie den Effekt der Aufrufe des Clients auf AtlAdvise und AtlUnadvise auffangen können.

Zum Beispiel können Sie Ihre Veranstaltung reimplementieren wie folgt (mit einigen Debugging-Ausgabe hinzugefügt):

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

mit der Debugging-Ausgabe fortsetzen können, können Sie ähnliche Diagnose an den MFC/ATL-Anwendung hinzufügen und genau sehen, Wenn der Referenzzähler auf der Sink-Schnittstelle aktualisiert wird (Beachten Sie, dass davon Debug-Builds von beide-Projekte vorausgesetzt werden). So zum Beispiel, ich habe eine Dump Methode zur Senke Implementierung hinzugefügt:

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

Dann die Debug-Client-Anwendung durch die IDE läuft, können Sie sehen, was passiert.Zunächst wurde bei der Erstellung des COM-Objekts:

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

Dies ergibt die folgende Debug-Ausgabe (können Sie die C# Spur von dem AtlAdvise Anruf dort sehen)

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)

Das sieht wie erwartet aus, wir haben eine Referenzzahl von 2 - eins aus dem nativen c ode AddRef und ein anderer (vermutlich) von AtlAdvise.

Jetzt können Sie überprüfen, was passiert, wenn die InvokeTestEvent() Methode aufgerufen wird - hier ich es zweimal:

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

Dies ist die entsprechende Spur ist

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

Sie, dass eine zusätzliche AddRef sehen ist passiert, das erste Mal, dass das Ereignis ausgelöst wird. Ich vermute, dass dies die Referenz ist, die erst bei der Garbage Collection veröffentlicht wird.

Schließlich, in OnDestroy, können wir sehen, dass die Referenzzahl wieder sinkt. Der Code ist

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

und die Trace-Ausgabe ist

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)

So können Sie sehen, dass AtlUnadvise nicht den Referenzzähler nicht beeinflusst (also noted by other people), aber Beachten Sie auch, dass wir eine Spur vom remove Accessor des C# COM Ob erhalten haben Ereignis, das ein möglicher Ort ist, um eine Müllsammlung oder andere Abbauaufgaben zu erzwingen.

Fassen wir zusammen:

  1. Sie berichtet Zugriffsverletzungen mit dem Code, den Sie geschrieben, aber ich konnte nicht diesen Fehler reproduzieren, so ist es möglich, dass der Fehler, den Sie für das Problem ist in keinem Zusammenhang sehen Sie beschrieben.
  2. Sie haben gefragt, wie Sie mit der COM-Client-Senke interagieren könnten, und ich habe einen möglichen Weg mit einer benutzerdefinierten Ereignisimplementierung gezeigt. Dies wird durch eine Debug-Ausgabe unterstützt, die zeigt, wie die beiden COM-Komponenten interagieren.

Ich hoffe wirklich, das ist hilfreich. Es gibt einige alternative Tipps zur COM-Handhabung und weitere Erläuterungen in this old but otherwise excellent blog post.

+0

Ich bin mir nicht sicher, ob Blogpost relevant ist. Es verbraucht ein COM-Objekt in verwaltetem Code und passt es so an, dass es deterministisch freigegeben wird. In meinem Fall wird das COM-Objekt selbst verwaltet und in nativem Code konsumiert. Im verwalteten Code habe ich nie einen Verweis auf die Ereignissenke, sodass ich ReleaseComObject() nicht aufrufen kann. Ich werde sehen, GC während event.remove() zwingen und sehen, ob das irgendwas tut. – Luke

+0

Das Erzwingen von GC während event.remove() scheint die Ref-Zählung nicht zu verringern. – Luke

+0

@Luke ja, tut mir leid, der Blog-Post ist da, weil ich es in der Vergangenheit nützlich fand, aber du hast recht, es ist für den umgekehrten Fall zu deinem. Außerdem habe ich GC.Collect versucht, aber ich denke, der COM-Wrapper wird immer noch offen gehalten, bis das Objekt selbst verschwunden ist. Mein Hauptanliegen war das Fehlen einer Zugriffsverletzung, so dass ich nicht fortfahren konnte zu versuchen, warum zu debuggen. Ich dachte, dass Sie in der Lage sein könnten, für dieses Problem eine Klärung vorzunehmen, indem Sie AtlUnadvise abfangen können. –

Verwandte Themen