我有一个用C#编写的COM服务器,一个用Delphi编写的COM客户端。我已经实现了一个简单而优雅的回调机制,它的工作原理就像一种魅力。然而,FastMM4报告说我的Delphi正在创建一个内存泄漏。我已经把这个应用程序提炼到了泄漏的源头。我有漏洞是由对象被引用计数的方式造成的(它永远不会变成零,所以永远不会被销毁),所以我试图理解为什么引用计数是这样工作的,而且是因为我在实现中做错了什么。
我已经尽可能地减少了代码,但是在一个问题中似乎仍然包含了很多代码。但我真的不知道怎么解释我在做什么。我将这两个项目(C#和Delphi)封装在一个压缩文件中,但是我似乎无法将其附加到任何地方。
我在C#端声明两个接口(ICOMCallbackContainer和ICOMCallbackTestServer),并在那里实现其中的一个(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实例从未被销毁,因为RefCount从未低于2。
因此,我的问题是,为什么将我的回调容器对象分配给COM属性会使引用计数增加两倍,以及为什么将零分配给COM属性根本不会减少引用计数?
编辑
我创建了TMyInterfacedObject (与TInterfacedObject相同),并将它用作TCOMCallbackContainer的基类。我在TMyInterfacedObject的每一种方法中都加了断点.在每个断点,我记录了调用堆栈(和其他一些信息)。对于每个更新RefCount的方法,行尾的数字显示了RefCount的新值。对于QueryInterface,我包括了IID和相应的接口名称(通过谷歌找到)和调用的结果。
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之后:
...
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
...发布于 2014-01-24 20:08:08
在托管代码中,外部COM接口封装到运行时可调用包装器 (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;
}
}https://stackoverflow.com/questions/21321552
复制相似问题