2013-06-21 22 views
7

Mam zarządzany obiekt COM napisany w języku C# oraz natywny klient COM i zlew napisany w C++ (MFC i ATL). Klient tworzy obiekt i doradza jego interfejsowi zdarzenia podczas uruchamiania, i nie radzi sobie ze swoim interfejsem zdarzeń i zwalnia obiekt podczas zamykania. Problem polega na tym, że obiekt COM odwołuje się do zlewu, który nie jest zwalniany, dopóki nie zostanie uruchomiony proces zbierania śmieci, w którym to momencie klient jest już zerwany, co zwykle skutkuje naruszeniem dostępu. Prawdopodobnie nie jest to wielka sprawa, ponieważ klient i tak się zamyka, ale chciałbym rozwiązać to z wdziękiem, jeśli to możliwe. Potrzebuję mój obiekt COM, aby zwolnić mój obiekt sink w bardziej punktualny sposób, i nie wiem, od czego zacząć, ponieważ mój obiekt COM nie działa jawnie z obiektem sink.Jak zarządzać długością obiektu podczas pracy z interfejsem COM?

Mój obiekt 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); 
     } 
    } 
} 

Klient jest standardowy program oparty na dialog MFC, z dodaną obsługą ATL. Moja klasa umywalką:

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

Mam następujący członkowie w mojej klasie dialogowym:

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

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

W 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

Wygląda na to, że zapomniałeś m_TestObjectEventsSink-> Release(). To nie jest automatyczne, ponieważ przechowujesz wskaźnik do CComObject <>, prawdopodobnie po prostu go przeciekasz. Nie wiem, dlaczego to by było konieczne. –

+0

Ups, przepraszam. Zapomniałem o nich, ale efekt jest taki sam jak CComObject :: CreateInstance() daje ci obiekt z liczbą ref 0. Będę aktualizować pytanie niezależnie. – Luke

+0

CComObject :: CreateInstance() daje obiekt o wartości ref 0; Twoim obowiązkiem jest AddRef() to. – Luke

Odpowiedz

3

Po pierwsze, powiem, że użyłem twojego przykładowy kod implementujący kopię tego, co opisałeś, ale nie widzę żadnych naruszeń dostępu podczas testowania kompilacji Debug lub Release.

Możliwe jest, że istnieje pewne alternatywne wytłumaczenie tego, co widzisz (np. Może zajść potrzeba połączenia się z Marshal.ReleaseCOMObject, jeśli masz inne interfejsy do natywnego klienta).

Istnieje wyczerpujący opis terminu/kiedy nie zadzwonić pod numer ReleaseCOMObject pod adresem MSDN here.

Mimo, że masz rację, że obiekt C# COM nie działa z klientem com za zlewu obiektu bezpośrednio, ale robi komunikować się z nim za pośrednictwem obiektu zdarzenia C#. Dzięki temu można zaimplementować niestandardowy obiekt zdarzenia, dzięki czemu można przechwycić efekt wywołań klienta na AtlAdvise i AtlUnadvise.

Na przykład, można reimplement wydarzenie następująco (z jakiegoś wyjścia debugowania dodanej):

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

kontynuować wyjście debugowania, można dodać podobne diagnostykę do aplikacji MFC/ATL i zobaczyć dokładnie gdy licznik odwołań jest aktualizowany w interfejsie sink (należy pamiętać, że zakłada to kompilację Debug dla obu projektów). Tak więc, na przykład, dodałem Dump metody do realizacji umywalką:

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

Następnie, uruchamiając aplikację kliencką Debug przez IDE, można zobaczyć, co się dzieje.Po pierwsze, podczas tworzenia obiektu 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")); 
    } 
} 

To daje następujące dane wyjściowe debugowania (można zobaczyć C# ślad od wezwania AtlAdvise tam)

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)

Wygląda to zgodnie z oczekiwaniami, mamy liczbę referencyjną 2 - jeden z natywnego c Od AddRef i innej (prawdopodobnie) od AtlAdvise.

Teraz można sprawdzić, co się dzieje, jeśli metoda InvokeTestEvent() nazywa - tutaj to zrobić dwa razy:

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

Jest odpowiedni ślad

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

Widać, że dodatkowy AddRef zdarzyło się, po raz pierwszy zdarzenie zostało wywołane. Zgaduję, że jest to odnośnik, który nie zostanie zwolniony do czasu wyrzucenia śmieci.

Wreszcie, w OnDestroy, możemy zobaczyć, że licznik odniesień ponownie spada. Kod jest

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

a wyjście ślad jest

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)

Więc widać, że AtlUnadvise nie wpływa na liczbę odwołań (also noted by other people), ale zauważmy również, że mamy ślad od remove uzyskującego C# COM ob zdarzenie ject, które jest możliwą lokalizacją do wymuszania niektórych zadań czyszczenia lub innych zadań odrywania.

Podsumowując:

  1. zgłoszono naruszeń dostępu z kodu, który pisał, ale nie mogę odtworzyć tego błędu, więc możliwe jest, że błąd widać nie ma związku z problemem opisałeś.
  2. Zapytałeś, w jaki sposób możesz wchodzić w interakcje ze zlewem klienta COM, a ja pokazałem jeden potencjalny sposób za pomocą niestandardowej implementacji zdarzenia. Jest to obsługiwane z wyjściem debugowania pokazującym, jak współdziałają dwa komponenty COM.

Mam nadzieję, że to będzie pomocne. Istnieje kilka alternatywnych porad dotyczących obsługi COM i więcej wyjaśnień w this old but otherwise excellent blog post.

+0

Nie jestem pewien, czy wpis na blogu jest trafny. Wykorzystuje obiekt COM w zarządzanym kodzie i dostosowuje go w sposób deterministyczny. W moim przypadku obiekt COM jest zarządzany i jest wykorzystywany w natywnym kodzie. W zarządzanym kodzie nigdy nie mam odniesienia do zlewu zdarzeń, więc nie mogę wywołać na nim funkcji ReleaseComObject(). Spojrzę na wymuszenie GC podczas event.remove() i zobaczę, czy to coś robi. – Luke

+0

Wymuszenie GC podczas event.remove() nie powoduje zmniejszenia liczby powtórzeń. – Luke

+0

@Luke tak, przepraszam, blog jest dostępny, ponieważ uznałem, że jest przydatny w przeszłości, ale masz rację, jeśli chodzi o przeciwny przypadek. Próbowałem też GC.Collect, ale myślę, że opakowanie COM jest wciąż otwarte, dopóki sam obiekt nie zniknie. Moim głównym zmartwieniem był brak naruszenia zasad dostępu, więc nie mogłem przystąpić do próby wyjaśnienia, dlaczego. Pomyślałem, że dzięki pułapce AtlUnadvise możesz być w stanie zastosować wyjaśnienie dla tego problemu. –

Powiązane problemy