首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >从C# COM dll回调到Delphi会导致内存泄漏

从C# COM dll回调到Delphi会导致内存泄漏
EN

Stack Overflow用户
提问于 2014-01-23 23:33:19
回答 1查看 1.1K关注 0票数 4

我有一个用C#编写的COM服务器,一个用Delphi编写的COM客户端。我已经实现了一个简单而优雅的回调机制,它的工作原理就像一种魅力。然而,FastMM4报告说我的Delphi正在创建一个内存泄漏。我已经把这个应用程序提炼到了泄漏的源头。我有漏洞是由对象被引用计数的方式造成的(它永远不会变成零,所以永远不会被销毁),所以我试图理解为什么引用计数是这样工作的,而且是因为我在实现中做错了什么。

我已经尽可能地减少了代码,但是在一个问题中似乎仍然包含了很多代码。但我真的不知道怎么解释我在做什么。我将这两个项目(C#和Delphi)封装在一个压缩文件中,但是我似乎无法将其附加到任何地方。

我在C#端声明两个接口(ICOMCallbackContainerICOMCallbackTestServer),并在那里实现其中的一个(COMCallbackTestServer)。我正在实现Delphi (TCOMCallbackContainer)上的另一个接口,并将Delphi类传递给C#类。

这是C# COM服务器:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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客户端:

代码语言:javascript
复制
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实例从未被销毁,因为RefCount从未低于2。

因此,我的问题是,为什么将我的回调容器对象分配给COM属性会使引用计数增加两倍,以及为什么将零分配给COM属性根本不会减少引用计数?

编辑

我创建了TMyInterfacedObject (与TInterfacedObject相同),并将它用作TCOMCallbackContainer的基类。我在TMyInterfacedObject的每一种方法中都加了断点.在每个断点,我记录了调用堆栈(和其他一些信息)。对于每个更新RefCount的方法,行尾的数字显示了RefCount的新值。对于QueryInterface,我包括了IID和相应的接口名称(通过谷歌找到)和调用的结果。

代码语言:javascript
复制
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

列出的所有断点都发生在FServer.CallbackContainer := TCOMCallbackContainer.Create(Process_Callback);语句中的TfrmMain.Create中。在破坏方法中,特别是在FServer.CallbackContainer := nil;语句中,没有击中任何一个断点.

我想,可能是在调用析构函数之前卸载了COM库,所以我将FServer.CallbackContainer := nil;行复制到构造函数的末尾。这没什么区别。

传递给QueryInterface调用的接口在Delphi环境中似乎不可用,因此我将尝试将其中一些接口继承到C#端的ICOMCallbackContainer中,以使它们可用(在研究了它们应该做什么以及它们应该如何工作之后)。

编辑2

我试着实现INoMarshal和IAgileObject,只是想看看会发生什么。我尝试了这两种方法,因为它们都是标记接口,没有什么可实际实现的。它稍微改变了这个过程,但没有起到任何作用。如果CLR找到了INoMarshal,那么它就不会查找IAgileObject或IMarshal,如果它找不到INoMarshal,但是找到了IAgileObject,那么它就不会查找IMarshal。(这似乎并不重要,甚至对我来说也没有意义。)

在将INoMarshal添加到TCOMCallbackContainer之后:

代码语言:javascript
复制
...
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之后:

代码语言:javascript
复制
...
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
...
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2014-01-24 20:08:08

在托管代码中,外部COM接口封装到运行时可调用包装器 (RCW)中。与原始COM接口不同,RCW的生存期由不使用引用计数的垃圾收集器确定。在您的特殊情况下,这意味着赋值为null并不会立即减少refCount。

可以通过显式调用Marshal.ReleaseComObject强制发布COM对象引用

代码语言:javascript
复制
     ICOMCallbackContainer ICOMCallbackTestServer.CallbackContainer
    {
        get { return _callbackContainer; }
        set { 

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

            _callbackContainer = value;
        }
    }
票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/21321552

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档