4

我有一个用 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");
        }
    }
}

这是德尔福回调容器:

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 实例永远不会被破坏,因为 RefCount 永远不会低于 2。

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

编辑

我创建了 TMyInterfacedObject(与 TInterfacedObject 相同)并将其用作 TCOMCallbackContainer 的基类。我在 TMyInterfacedObject 的每个方法中都设置了断点。在每个断点,我都记录了调用堆栈(以及其他一些信息)。对于每个更新 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

列出的所有断点都发生在FServer.CallbackContainer := TCOMCallbackContainer.Create(Process_Callback);TfrmMain.Create 的语句中。在 Destroy 方法中,尤其是在FServer.CallbackContainer := nil;语句中,没有一个断点被命中。

我想,也许是 COM 库在调用析构函数之前就被卸载了,所以我将这FServer.CallbackContainer := nil;一行复制到构造函数的末尾。它没有任何区别。

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

编辑 2

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

将 INoMarshal 添加到 TCOMCallbackContainer 后:

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

1 回答 1

4

在托管代码中,外部 COM 接口被包装到Runtime Callable Wrapper (RCW) 中。与原始 COM 接口不同,RCW 寿命由不使用引用计数的垃圾收集器确定。在您的特定情况下,这意味着对 null 的分配不会立即减少 refCount。

可以通过显式调用Marshal.ReleaseComObject来强制释放 COM 对象引用:

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

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

            _callbackContainer = value;
        }
    }
于 2014-01-24T20:08:08.480 回答