从C#COM DLL到Delphi应用的回调导致内存泄漏



我有一个用C#编写的com服务器,还有一个用delphi编写的com客户端。我已经实施了一种简单而优雅的呼叫机制,它就像魅力一样。但是,FastMM4报告了我的Delphi客户端正在创建内存泄漏。我已经将应用程序提炼到了泄漏来自何处的本质。我的泄漏是由对象被计数的方式引起的(它永远不会零,因此永远不会被破坏),因此我试图理解为什么参考计数是按照其方式工作的,并且是它是因为我在实施中做错了。

我已经尽可能地将代码切下来,但是在一个问题中似乎仍然很多。但是我真的不知道该如何解释我在做什么。我有两个项目(C#和Delphi)包裹在zip文件中,但是我似乎无法在任何地方附加。

我在C#侧(ICOMCallbackContainerICOMCallbackTestServer)上声明两个接口,并在此处实现其中一个(COMCallbackTestServer)。我正在Delphi侧(TCOMCallbackContainer)上实现其他接口,并将Delphi类传递到C#类。

这是C#com服务器:

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

这是Delphi CallbackContainer:

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从tautointfobject继承,因此实现了IDISPATCH。我不知道我是否在构造函数中做正确的事情。我对如何使用idispatch不太熟悉。

这是Delphi 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;

上面的tcomcallbackcontainer实例从未被销毁,因为重新数从未低于2。

所以我的问题是,为什么将我的回调容器对象分配给com属性将参考计数增加两个,为什么将零分配给com属性完全不降低参考计数?

编辑

我创建了TmyInterFacedObject(与TinterFacedObject相同),并将其用作TcomcallbackContainer的基类。我在每种TmyInterFacedObject的方法中都放了突破点。在每个断点点,我记录了呼叫堆栈(以及其他一些信息)。对于更新重新数的每种方法,该行末尾的数字显示了重新数的新值。对于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);语句中。在销毁方法中,尤其是在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):  S_OK
CLR -> TInterfacedObject.QueryInterface -> TObject.GetInterface -> _AddRef:  3
CLR -> TInterfacedObject._Release:  2
CLR -> TInterfacedObject.QueryInterface(IRpcOptions):  E_NOINTERFACE
CLR -> TInterfacedObject._Release:  1
...

将iagileObject添加到tcomcallbackContainer:

...
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
...

在托管代码外部com接口中包裹在运行时可呼叫包装器(RCW)中。与RAW COM接口不同,RCW寿命由不使用参考计数的垃圾收集器确定。在您的特殊情况下,这意味着对null的分配不会立即减少重新数。

com对象参考版本可以通过明确调用元帅来强制强制。releasecomobject:

     ICOMCallbackContainer ICOMCallbackTestServer.CallbackContainer
    {
        get { return _callbackContainer; }
        set { 
            if (_callbackContainer != null)
            {
                  Marshal.ReleaseComObject(_callbackContainer); // calls IUnknown.Release()
                  _callbackContainer = null;
            }
            _callbackContainer = value;
        }
    }

最新更新