2013-06-21 45 views
7

Tôi có đối tượng COM được quản lý được viết bằng C# và trình khách COM gốc và chìm được viết bằng C++ (MFC và ATL). Máy khách tạo đối tượng và tư vấn cho giao diện sự kiện của nó khi khởi động và các unadvises từ giao diện sự kiện của nó và giải phóng đối tượng lúc tắt máy. Vấn đề là đối tượng COM có một tham chiếu đến bồn rửa mà không được phát hành cho đến khi thu gom rác chạy, lúc đó máy khách đã bị rách và do đó thường dẫn đến vi phạm truy cập. Nó có lẽ không phải là một thỏa thuận lớn vì khách hàng đang đóng cửa, nhưng tôi muốn giải quyết điều này một cách duyên dáng nếu có thể. Tôi cần đối tượng COM của tôi để giải phóng đối tượng chìm của tôi một cách kịp thời hơn, và tôi không thực sự biết bắt đầu từ đâu đối tượng COM của tôi không làm việc với đối tượng sink một cách rõ ràng.Làm thế nào để quản lý tuổi thọ đối tượng khi làm việc với COM interop?

đối tượng COM của tôi:

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

Các khách hàng là một chương trình MFC thoại dựa trên tiêu chuẩn, với hỗ trợ thêm cho ATL. lớp chìm của tôi:

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

tôi có các thành viên sau đây trong lớp thoại của tôi:

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

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

Trong 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

Có vẻ như tôi đang quên m_TestObjectEventsSink-> Release(). Nó không phải là tự động kể từ khi bạn lưu trữ một con trỏ đến CComObject <>, bạn có thể chỉ bị rò rỉ nó. Không chắc tại sao điều đó lại cần thiết. –

+0

Rất tiếc, xin lỗi. Quên về những điều đó, nhưng hiệu ứng cũng giống như CComObject :: CreateInstance() cung cấp cho bạn một đối tượng với số lượng ref là 0. Tôi sẽ cập nhật câu hỏi bất kể. – Luke

+0

CComObject :: CreateInstance() cung cấp cho bạn một đối tượng với số lượng ref là 0; đó là trách nhiệm của bạn đối với AddRef() nó. – Luke

Trả lời

3

Trước hết, tôi sẽ chỉ nói rằng tôi đã sử dụng mã ví dụ để triển khai bản sao của những gì bạn đã mô tả, nhưng tôi không thấy bất kỳ vi phạm truy cập nào khi kiểm tra bản dựng Gỡ lỗi hoặc Bản phát hành.

Vì vậy, có thể có một số giải thích thay thế cho những gì bạn đang thấy (ví dụ: bạn có thể cần phải gọi Marshal.ReleaseCOMObject nếu bạn giữ các giao diện khác cho ứng dụng khách gốc).

Có mô tả toàn diện về thời điểm/khi không gọi ReleaseCOMObject trên MSDN here.

Có nói rằng, bạn nói đúng rằng C# đối tượng COM của bạn không làm việc với đối tượng chìm client COM của trực tiếp, nhưng nó không giao tiếp với nó thông qua C# sự kiện đối tượng. Điều này cho phép bạn triển khai đối tượng sự kiện tùy chỉnh để bạn có thể bẫy ảnh hưởng của các cuộc gọi của khách hàng đến AtlAdviseAtlUnadvise.

Ví dụ, bạn có thể reimplement sự kiện của bạn như sau (với một số đầu ra gỡ lỗi gia tăng):

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

Tiếp tục với sản lượng gỡ lỗi, bạn có thể thêm chẩn đoán tương tự như các ứng dụng MFC/ATL và xem chính xác khi số lượng tham chiếu được cập nhật trên giao diện sink (lưu ý rằng điều này giả định các bản dựng Debug của cả hai dự án). Vì vậy, ví dụ, tôi đã thêm một phương pháp Dump đến việc thực hiện chìm:

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

Sau đó, chạy ứng dụng client gỡ lỗi thông qua IDE, bạn có thể xem những gì đang xảy ra.Thứ nhất, trong quá trình tạo của đối tượng 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")); 
    } 
} 

Điều này cho phép kết xuất debug sau (bạn sẽ nhìn thấy C# trace từ AtlAdvise cuộc gọi trong đó)

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)

Điều này có vẻ như dự kiến, chúng tôi có số lượng tham chiếu 2 - một từ nguồn gốc c ode AddRef và một số khác (có lẽ) từ AtlAdvise.

Bây giờ, bạn có thể kiểm tra những gì sẽ xảy ra nếu các phương pháp InvokeTestEvent() được gọi là - ở đây tôi làm điều đó hai lần:

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

Đây là dấu vết tương ứng

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

Bạn có thể thấy rằng thêm một AddRef đã xảy ra, lần đầu tiên sự kiện được kích hoạt. Tôi đoán rằng đây là tài liệu tham khảo không được phát hành cho đến khi thu gom rác.

Cuối cùng, trong OnDestroy, chúng tôi có thể thấy số lượng tham chiếu giảm xuống một lần nữa. Mã này là

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

và dấu vết đầu ra là

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)

Vì vậy, bạn có thể thấy rằng AtlUnadvise không ảnh hưởng đến số lượng tài liệu tham khảo (also noted by other people) nhưng cũng lưu ý rằng chúng tôi có một dấu vết từ truy cập remove của C# COM ob sự kiện ject, đó là một vị trí có thể để buộc một số bộ sưu tập rác hoặc các công việc rách khác.

Để tóm tắt:

  1. Bạn đã báo cáo vi phạm truy cập với mã bạn được đăng nhưng tôi không thể tái sản xuất lỗi đó, vì vậy nó có thể là lỗi mà bạn thấy là không liên quan đến vấn đề bạn mô tả.
  2. Bạn đã hỏi cách bạn có thể tương tác với bồn rửa khách COM và tôi đã hiển thị một cách tiềm năng bằng cách sử dụng triển khai sự kiện tùy chỉnh. Điều này được hỗ trợ với đầu ra gỡ lỗi cho thấy cách hai thành phần COM tương tác.

Tôi thực sự hy vọng điều này hữu ích. Có một số mẹo xử lý COM thay thế và giải thích thêm trong this old but otherwise excellent blog post.

+0

Tôi không chắc chắn rằng bài đăng trên blog có liên quan. Nó tiêu thụ một đối tượng COM trong mã được quản lý, và tùy biến nó để được phát hành một cách xác định. Trong trường hợp của tôi, chính đối tượng COM được quản lý và được tiêu thụ trong mã gốc. Trong mã quản lý tôi không bao giờ có một tham chiếu đến sự kiện chìm vì vậy tôi không thể gọi ReleaseComObject() trên đó. Tôi sẽ xem xét buộc GC trong event.remove() và xem nếu điều đó làm bất cứ điều gì. – Luke

+0

Buộc GC trong event.remove() không xuất hiện để giảm số lượng ref. – Luke

+0

@Luke vâng, xin lỗi, bài đăng trên blog có ở đó vì tôi thấy nó hữu ích trong quá khứ nhưng bạn nói đúng cho trường hợp ngược lại với bạn. Ngoài ra, tôi đã thử GC.Collect quá nhưng tôi nghĩ rằng wrapper COM vẫn đang được tổ chức mở cho đến khi đối tượng chính nó đã biến mất. Mối quan tâm chính của tôi là việc thiếu vi phạm truy cập mặc dù vậy tôi không thể tiếp tục cố gắng gỡ lỗi tại sao. Tôi nghĩ rằng bằng cách có thể bẫy AtlUnadvise, bạn có thể áp dụng xóa cho vấn đề đó. –