2014-01-23 2 views
4

C#으로 작성된 COM 서버와 Delphi로 작성된 COM 클라이언트가 있습니다. 간단하고 우아한 콜백 메커니즘을 구현했으며 매력처럼 작동합니다. 그러나 FastMM4는 Delphi 클라이언트가 메모리 누수를 일으키고 있다고보고합니다. 나는 누수가 어디서 왔는지에 대한 본질에 응용 프로그램을 증류했습니다. 나는 그 누수가 객체가 참조 카운팅되는 방식으로 발생합니다 (절대로 0이되지 않으므로 절대 파손되지 않습니다). 그래서 참조 카운팅이 작동하는 방식을 이해하고 있는지 이해하려고합니다. 그것은 내가 구현에서 잘못하고있는 무언가 때문입니다.C# COM dll에서 Delphi 앱으로 콜백하면 메모리 누수가 발생합니다.

나는 가능한 한 코드를 줄 였지만 여전히 질문에 포함시키는 것이 많이 보인다. 그러나 나는 정말로 내가하고있는 것을 어떻게 설명 해야할지 모르겠다. 두 프로젝트 (C# 및 Delphi)를 zip 파일로 정리하고 멋지게 꾸며 냈습니다. 그러나 어디에도 첨부 할 수있는 것처럼 보이지 않습니다.

C# 측 (ICOMCallbackContainerICOMCallbackTestServer)의 두 인터페이스를 선언하고 거기에 그 중 하나를 구현합니다 (COMCallbackTestServer). 저는 델파이 측 (TCOMCallbackContainer)의 다른 인터페이스를 구현하고 있으며 델파이 클래스를 C# 클래스에 전달하고 있습니다.

namespace COMCallbackTest 
{ 
    [ComVisible(true)] 
    [Guid("2AB7E954-0AAF-4CFE-844C-756E50FE6360")] 
    public interface ICOMCallbackContainer 
    { 
     void Callback(string message); 
    } 

    [ComVisible(true)] 
    [Guid("7717D7AE-B763-48BC-BA0B-0F3525BEE8A4")] 
    public interface ICOMCallbackTestServer 
    { 
     ICOMCallbackContainer CallbackContainer { get; set; } 
     void RunCOMProcess(); 
     void Dispose(); 
    } 

    [ComVisible(true)] 
    [Guid("CF33E3A7-0886-4A0D-A740-537D0640C641")] 
    public class COMCallbackTestServer : ICOMCallbackTestServer 
    { 
     ICOMCallbackContainer _callbackContainer; 

     ICOMCallbackContainer ICOMCallbackTestServer.CallbackContainer 
     { 
      get { return _callbackContainer; } 
      set { _callbackContainer = value; } 
     } 

     void ICOMCallbackTestServer.RunCOMProcess() 
     { 
      if (_callbackContainer != null) 
      { 
       _callbackContainer.Callback("Step One"); 
       _callbackContainer.Callback("Step Two"); 
       _callbackContainer.Callback("Step Three"); 
      } 
     } 

     void ICOMCallbackTestServer.Dispose() 
     { 
      if (_callbackContainer != null) 
       _callbackContainer.Callback("Done"); 
     } 
    } 
} 

이것은 델파이 CallbackContainer입니다 : TAutoIntfObject에서

type 
    TCOMCallbackMethod = reference to procedure(AMessage: string); 

    TCOMCallbackContainer = class(TAutoIntfObject, ICOMCallbackContainer) 
    private 
    FCallbackMethod: TCOMCallbackMethod; 
    procedure Callback(const message: WideString); safecall; 
    public 
    constructor Create(ACallbackMethod: TCOMCallbackMethod); 
    destructor Destroy; override; 
    end; 

// ... 

constructor TCOMCallbackContainer.Create(ACallbackMethod: TCOMCallbackMethod); 
var 
    typeLib: ITypeLib; 
begin 
    OleCheck(LoadRegTypeLib(LIBID_COMCallbackTestServer, 
          COMCallbackTestServerMajorVersion, 
          COMCallbackTestServerMinorVersion, 
          0, 
          {out} typeLib)); 
    inherited Create(typeLib, ICOMCallbackContainer); 
    FCallbackMethod := ACallbackMethod; 
end; 

destructor TCOMCallbackContainer.Destroy; 
begin 
    FCallbackMethod := nil; 

    inherited Destroy; 
end; 

procedure TCOMCallbackContainer.Callback(const message: WideString); 
begin 
    if Assigned(FCallbackMethod) then 
    FCallbackMethod(message); 
end; 

TCOMCallbackContainer의 inherites가 IDispatch를 구현 있도록

이는 C#의 COM 서버입니다. 나는 내가 생성자에서 옳은 일을하는지 알지 못한다. 나는 내가 원하는 것처럼 IDispatch를 사용하는 방법에 익숙하지 않다.

는 델파이 COM 클라이언트입니다 :

procedure TfrmMain.FormCreate(Sender: TObject); 
begin 
    FServer := CoCOMCallbackTestServer_.Create as ICOMCallbackTestServer; 

    // Increments RefCount by 2, expected 1 
    FServer.CallbackContainer := TCOMCallbackContainer.Create(Process_Callback); 
end; 

procedure TfrmMain.FormDestroy(Sender: TObject); 
begin 
    // Decrements RefCount by 0, expected 1 
    FServer.CallbackContainer := nil; 

    FServer.Dispose; 
    FServer := nil; 
end; 

procedure TfrmMain.btnBeginProcessClick(Sender: TObject); 
begin 
    FServer.RunCOMProcess; 
end; 

procedure TfrmMain.Process_Callback(AMessage: string); 
begin 
    mmoProcessMessages.Lines.Add(AMessage); 
end; 

refcount가 결코 그래서 제 질문은, 왜 내 콜백 컨테이너 개체를 할당 않고있다

2. 아래 얻을 수 없기 때문에 파괴 결코 극복 위 TCOMCallbackContainer의 인스턴스 COM 속성에 참조 횟수를 2 씩 늘리면 COM 속성에 nil을 할당해도 참조 횟수가 줄어들지 않습니다.

EDIT 난 (의 TInterfacedObject 동일) TMyInterfacedObject을 만들어 TCOMCallbackContainer 기본 클래스로 사용 하였다. TMyInterfacedObject의 각 메서드에 중단 점을 넣습니다. 각 브레이크 포인트에서 나는 call-stack (그리고 다른 정보들)을 기록했다. RefCount를 업데이트하는 각 메서드에 대해 줄 끝의 숫자는 RefCount의 새 값을 표시합니다. QueryInterface의 경우 IID와 Google에서 찾은 해당 인터페이스 이름과 호출 결과를 포함했습니다.

TfrmMain.FormCreate -> TCOMCallbackContainer.Create -> TInterfacedObject.NewInstance: 1 
TfrmMain.FormCreate -> TCOMCallbackContainer.Create -> TInterfacedObject.AfterConstruction: 0 
CLR -> TInterfacedObject.QueryInterface("00000000-0000-0000-C000-000000000046" {IUnknown}): S_OK 
CLR -> TInterfacedObject.QueryInterface -> TObject.GetInterface -> _AddRef: 1 
CLR -> TInterfacedObject.QueryInterface("C3FCC19E-A970-11D2-8B5A-00A0C9B7C9C4" {IManagedObject}): E_NOINTERFACE 
CLR -> TInterfacedObject.QueryInterface("B196B283-BAB4-101A-B69C-00AA00341D07" {IProvideClassInfo}): E_NOINTERFACE 
CLR -> TInterfacedObject._AddRef: 2 
CLR -> TInterfacedObject.QueryInterface("ECC8691B-C1DB-4DC0-855E-65F6C551AF49" {INoMarshal}): E_NOINTERFACE 
CLR -> TInterfacedObject.QueryInterface("94EA2B94-E9CC-49E0-C0FF-EE64CA8F5B90" {IAgileObject}): E_NOINTERFACE 
CLR -> TInterfacedObject.QueryInterface("00000003-0000-0000-C000-000000000046" {IMarshal}): E_NOINTERFACE 
CLR -> TInterfacedObject.QueryInterface("00000144-0000-0000-C000-000000000046" {IRpcOptions}): E_NOINTERFACE 
CLR -> TInterfacedObject._Release: 1 
CLR -> TInterfacedObject.QueryInterface("2AB7E954-0AAF-4CFE-844C-756E50FE6360" {ICOMCallbackContainer}): S_OK 
CLR -> TInterfacedObject.QueryInterface -> TObject.GetInterface -> _AddRef: 2 
CLR -> TInterfacedObject._AddRef: 3 
CLR -> TInterfacedObject._Release: 2 

나와있는 브레이크 포인트의 모든 TfrmMain.Create 내 FServer.CallbackContainer := TCOMCallbackContainer.Create(Process_Callback); 성명에서 일어났다. Destroy 메서드에서는 특히 FServer.CallbackContainer := nil; 문에서 중단 점이 하나도 발생하지 않았습니다.

아마도 소멸자가 호출되기 전에 COM 라이브러리가 언로드되었다고 생각하여 FServer.CallbackContainer := nil; 행을 생성자의 끝에 복사했습니다. 아무런 차이가 없었습니다.

QueryInterface 호출에 전달 된 인터페이스가 Delphi 환경에서 사용 가능하지 않은 것 같아서 일부를 C# 측의 ICOMCallbackContainer에 상속하여 사용 가능하게하려고합니다. 다시해야하고 어떻게 작동해야하는지).

편집 2

난 그냥 무슨 일이 일어날 지보고 INoMarshal 및 IAgileObject을 구현했습니다. 이 둘은 마커 인터페이스이고 실제로 구현할 것이 없었기 때문에 두 가지를 시도했습니다. 프로세스가 조금 변경되었지만 어떤 방식 으로든 도움이되지는 않았습니다. CLR이 INoMarshal을 찾으면 IAgileObject 또는 IMarshal을 찾지 않으며 INoMarshal을 찾지 못했지만 IAgileObject를 찾으면 IMarshal을 찾지 않는 것 같습니다. (이 문제, 또는 나에게 이해하는 것하지 않는 것이.)

을 TCOMCallbackContainer에 INoMarshal을 추가 한 후 :

... 
CLR -> TInterfacedObject._AddRef: 2 
CLR -> TInterfacedObject.QueryInterface(INoMarshal): E_NOINTERFACE 
CLR -> TInterfacedObject.QueryInterface(IAgileObject): S_OK 
CLR -> TInterfacedObject.QueryInterface -> TObject.GetInterface -> _AddRef: 3 
CLR -> TInterfacedObject._Release: 2 
CLR -> TInterfacedObject.QueryInterface(IRpcOptions): E_NOINTERFACE 
CLR -> TInterfacedObject._Release: 1 
... 
+0

합니까'[대해 ComVisible이 (사실)]'string'가'BStr'로 마샬링 '암시? –

+1

'TCOMCallbackContainer.Destroy'는 무의미합니다. 그냥 제거하십시오. 당신의'TAutoIntfObject' 하위 클래스는 형식이 VCL의 유일한 클래스, 즉'TStringsAdapter'와 동일합니다. 그래서'TCOMCallbackContainer'는 괜찮다고 생각합니다. COM 서버는 어떻게 가져 왔습니까? 그게 합리적으로 보입니까? 그것은 우리가 볼 수없는 자동 생성 코드입니다. 그래서'CoCOMCallbackTestServer_' 형과 친구들. –

+0

@DavidHeffernan, 문자열 유형은 _TLB.pas 파일에 WideString으로 표시됩니다.이 문자열 유형은 BStr과 동일하다고 생각합니다. –

답변

4

를 관리 코드 외부를에서 :

... 
CLR -> TInterfacedObject._AddRef: 2 
CLR -> TInterfacedObject.QueryInterface(INoMarshal): S_OK 
CLR -> TInterfacedObject.QueryInterface -> TObject.GetInterface -> _AddRef: 3 
CLR -> TInterfacedObject._Release: 2 
CLR -> TInterfacedObject.QueryInterface(IRpcOptions): E_NOINTERFACE 
CLR -> TInterfacedObject._Release: 1 
... 

TCOMCallbackContainer에 IAgileObject를 추가 한 후 COM 인터페이스는 Runtime Callable Wrapper (RCW)에 래핑됩니다. 원시 COM 인터페이스와 달리 RCW 수명은 참조 횟수를 사용하지 않는 가비지 수집기에 의해 결정됩니다. 특별한 경우 null 로의 할당이 refCount를 즉시 감소시키지 않는다는 것을 의미합니다.

COM 객체 참조 자료는 명시 적으로 Marshal.ReleaseComObject를 호출하여 강제 할 수 있습니다

 ICOMCallbackContainer ICOMCallbackTestServer.CallbackContainer 
    { 
     get { return _callbackContainer; } 
     set { 

      if (_callbackContainer != null) 
      { 
        Marshal.ReleaseComObject(_callbackContainer); // calls IUnknown.Release() 
        _callbackContainer = null; 
      } 

      _callbackContainer = value; 
     } 
    } 
+0

정말 고마워요. 이 현상은 내가 경험 한 누출을 고칠뿐만 아니라 뭔가를 배웠습니다. 마샬링에 대해 자세히 알아야합니다. –

관련 문제