在释放DLL中的接口对象时出现无效指针操作错误

3

我仍在努力掌握使用接口。我将它们实现的目的是与在DLL中实例化的对象进行交互。当我使用它时,一切都正常工作,所有方法都按预期工作等等。问题是在清理该接口后面的对象时。

我有一个简单的接口如下所示

IMyInterface = interface
  ['{d52b14f3-156b-4df8-aa16-cb353193d27c}']
  procedure Foo;
end;

还需要一个相应的对象

TMyObject = class(TInterfacedObject, IMyInterface)
private
  procedure Foo;
end;

在DLL内部,我有一个全局变量作为该对象的实例以及两个导出函数来创建和销毁此实例。

var
  _MyObject: TMyObject;

function CreateMyObject: IMyInterface; stdcall;
begin
  _MyObject:= TMyObject.Create;
  Result:= IMyInterface(_MyObject);
end;

function DestroyMyObject: Integer; stdcall;
begin
  _MyObject.Free; //   <--   Invalid Pointer Operation
end;

这个对象的析构函数几乎什么都不做,只是继承。但我仍然遇到了这个问题。但在执行 _MyObject.Free 时,我得到了 Invalid Pointer Operation 的错误。

我使用 LoadLibraryGetProcAddress 访问这些导出方法。

为什么会发生这种情况,我应该如何解决?


当调用者两次调用 CreateMyObject 时会发生什么?为什么你需要一个全局变量呢?你只需要一个返回由新创建的对象实现的接口的函数。 - David Heffernan
@David 实际上,我上面的代码是一个更复杂基础设施的重新制定版本。我实际上正在维护这些实例的列表,但是发布如何管理此列表的代码将会产生大量不必要的代码,因此我基本上只展示了事物如何工作的核心基础知识。 - Jerry Dodge
2个回答

6
一个无效的指针操作意味着你释放了一个未分配的东西。
在这种情况下,你正在释放的对象已经被销毁。请在析构函数中设置断点并自行查看。
接口与之相关的是引用计数代码,这就是为什么你所读到的有关接口的所有建议都说不要将它们与对象引用混合使用,因为后者没有这种引用计数。
当你实例化对象并将其分配给全局变量时,对象的引用计数为零,并且还没有涉及接口。当你将其分配给函数结果时,引用计数变为一。如果你启用了调试 DCU 并逐步执行此语句,则可以观察到发生了什么。(顺便说一下,类型转换是不必要的;编译器已经知道对象实现了目标接口,并将允许简单赋值语句本身。)
在 DLL 的使用方面,保存对象最后一个接口引用的变量会被清除,引用计数变为零,对象自我销毁。
一旦对象被销毁,你的全局变量就是悬挂引用。它保存的是一个不存在的对象的地址。当你在它上面调用 Free 时,析构函数将地址传递给内存管理器,但内存管理器知道它没有该地址的任何内容(了),因此会引发异常。
要修复这个问题,请将全局变量的类型更改为接口类型,然后删除 Free 调用;将其替换为一个语句,将 nil 赋值给变量。通过这些更改,在创建对象并在变量中存储接口引用时,将设置对象的引用计数为一,并将其返回给调用者将其设置为两个。当使用者清除它的引用时,计数将降低到一,新的 nil 分配将使它变为零,在适当的时间释放对象。
一旦你开始通过接口引用访问一个对象,最好不要再使用普通对象引用。风险太大了,容易在对象已经被销毁后意外使用它。

另一个选择是通过实现AddRef和Release方法来删除引用计数,以始终具有(比如)1的值;然后您可以像非引用计数对象引用一样手动释放对象。 - David
@David,返回值与删除引用计数无关。要删除引用计数,只需将它们实现为不计数即可。但这并不能解决所有问题。如果你有一个悬空的接口引用,你很可能会遇到访问违规或其他未定义行为。 - Rob Kennedy
天啊,现在我不喜欢接口 [实际上我不喜欢引用计数]。从来没想过我不应该释放它。 - Jerry Dodge
@RobKennedy,抱歉我不理解你的评论。我已经阅读了你的答案,因为接口在引用计数达到0时会自动释放,所以混合对象和接口引用已经被释放。因此,一种常见的解决方案是禁用引用计数并手动释放接口对象。你的评论或关于“返回值”的问题中我错过了什么? - David
@David,你说去除引用计数的方法是实现AddRefRelease并返回1。返回值无关紧要;没有生产代码会使用该值。要禁用引用计数,在这些方法中可以执行任何操作,但不要计算引用。这并不能解决很多问题,因为即使您不计算接口引用,也并不意味着不存在引用。如果存在引用,并且您手动释放对象,则程序可能仍然会崩溃。如果有什么问题,保持计算引用,但禁用自我释放行为。 - Rob Kennedy
@RobKennedy: “1”是随意的,它防止了引用被计算,正如您建议的那样。悬挂引用是另一个问题(设计问题),但至少这可以防止在混合对象/接口引用时释放对象。这只是一个建议!这是一种常见的Delphi技术。请参见https://dev59.com/VnRA5IYBdhLWcg3w9ivq和https://dev59.com/nWw05IYBdhLWcg3wsz7N和http://blogs.teamb.com/craigstuntz/2008/01/23/37786/。 - David

2

不应该在TInterfacedObject-based类上调用.Free方法。当最后一个使用接口的引用被置为nil时,它会自动释放。

你的代码示例应该像这样:

var
  _MyObject: IUnknown;

function CreateMyObject: IMyInterface; stdcall;
begin
  // unified interface
  _MyObject:= TMyObject.Create as IUnknown;
  // cast to IMyInterface
  Result:= _MyObject as IMyInterface;
end;

function DestroyMyObject: Integer; stdcall;
begin
  _MyObject := nil; 
end;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接