编译器对隐式接口变量的处理是否有文档记录?

86

我不久前曾经提出一个类似的问题,关于隐式接口变量。

这个问题的根源是由于我的代码中存在一个隐式接口变量,而我没有意识到编译器会自动生成这个变量。当拥有该变量的过程结束时,它将被终结。这导致了一个错误,因为变量的生命周期比我预期的要长。

现在,我有一个简单的项目来说明编译器一些有趣的行为:

program ImplicitInterfaceLocals;

{$APPTYPE CONSOLE}

uses
  Classes;

function Create: IInterface;
begin
  Result := TInterfacedObject.Create;
end;

procedure StoreToLocal;
var
  I: IInterface;
begin
  I := Create;
end;

procedure StoreViaPointerToLocal;
var
  I: IInterface;
  P: ^IInterface;
begin
  P := @I;
  P^ := Create;
end;

begin
  StoreToLocal;
  StoreViaPointerToLocal;
end.

StoreToLocal被编译的方式和你想象的一样。函数结果的局部变量I会隐式传递给Create作为一个var参数。对于StoreToLocal的清理只需要一次调用IntfClear,没有什么意外。

然而,StoreViaPointerToLocal被处理的方式不同。编译器创建了一个隐式的局部变量,将其传递给Create。当Create返回时,会执行对P^的赋值操作。这使得该例程有两个持有接口引用的局部变量。对于StoreViaPointerToLocal的清理需要调用两次IntfClear

StoreViaPointerToLocal的编译代码如下:

ImplicitInterfaceLocals.dpr.24: begin
00435C50 55               push ebp
00435C51 8BEC             mov ebp,esp
00435C53 6A00             push $00
00435C55 6A00             push $00
00435C57 6A00             push $00
00435C59 33C0             xor eax,eax
00435C5B 55               push ebp
00435C5C 689E5C4300       push $00435c9e
00435C61 64FF30           push dword ptr fs:[eax]
00435C64 648920           mov fs:[eax],esp
ImplicitInterfaceLocals.dpr.25: P := @I;
00435C67 8D45FC           lea eax,[ebp-$04]
00435C6A 8945F8           mov [ebp-$08],eax
ImplicitInterfaceLocals.dpr.26: P^ := Create;
00435C6D 8D45F4           lea eax,[ebp-$0c]
00435C70 E873FFFFFF       call Create
00435C75 8B55F4           mov edx,[ebp-$0c]
00435C78 8B45F8           mov eax,[ebp-$08]
00435C7B E81032FDFF       call @IntfCopy
ImplicitInterfaceLocals.dpr.27: end;
00435C80 33C0             xor eax,eax
00435C82 5A               pop edx
00435C83 59               pop ecx
00435C84 59               pop ecx
00435C85 648910           mov fs:[eax],edx
00435C88 68A55C4300       push $00435ca5
00435C8D 8D45F4           lea eax,[ebp-$0c]
00435C90 E8E331FDFF       call @IntfClear
00435C95 8D45FC           lea eax,[ebp-$04]
00435C98 E8DB31FDFF       call @IntfClear
00435C9D C3               ret 
我能猜出编译器这样做的原因。当它可以证明将值分配给结果变量不会引发异常时(即如果变量是局部变量),则直接使用结果变量。否则,它使用隐式局部变量,并在函数返回后复制接口,从而确保我们在出现异常时不会泄露引用。
但我在文档中找不到任何关于这一点的说明。这很重要,因为接口的生命周期很重要,作为程序员,您需要偶尔影响它。
那么,有人知道是否有关于这种行为的文档吗?如果没有,有人对此有更多了解吗?实例字段如何处理,我还没有检查过。当然,我可以自己尝试所有这些内容,但我正在寻找更正式的声明,并且始终更喜欢避免依赖通过试错得出的实现细节。
更新1
回答Remy的问题,当我需要在执行另一个终结之前完成接口后面的对象时,这很重要。
begin
  AcquirePythonGIL;
  try
    PyObject := CreatePythonObject;
    try
      //do stuff with PyObject
    finally
      Finalize(PyObject);
    end;
  finally
    ReleasePythonGIL;
  end;
end;

按照这样的方式编写是可以的。但在真实代码中我有一个隐式的局部变量,它在GIL释放后才进行清理,结果就出问题了。我通过将Acquire/Release GIL中的代码提取到单独的方法中来解决了这个问题,从而缩小了接口变量的范围。


8
不知道为什么会被踩,除了问题确实很复杂以外。因为太高深了所以点赞。我知道这个诡秘的部分导致了我在一年前所工作的应用程序中一些微妙的引用计数错误。我们最好的极客花了数小时来解决它。最终我们通过绕过它来解决了问题,但从未理解编译器的预期工作方式。 - Warren P
3
编译器的引用计数没问题。问题在于有一个额外的变量持有一个引用,但我看不到它。我想知道的是是什么导致编译器会产生这样一个额外的隐藏引用。 - David Heffernan
3
我理解您的意思,但是写出不依赖这些额外变量的代码是一个好习惯。让编译器随意创建这些变量,而坚实的代码不应该依赖它们。 - kludg
2
另一个例子是这样的:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end; - Ondrej Kelle
2
我很想称之为编译器错误...临时变量应该在超出其作用域后被清除,这应该是赋值结束的时候(而不是函数结束)。如果不这样做,就会产生你发现的微妙错误。 - nneonneo
显示剩余15条评论
2个回答

15
如果有关于这种行为的任何文档,它可能会涉及编译器在将函数结果作为参数传递时生成临时变量来保存中间结果的区域。考虑以下代码:

如果有关于这种行为的任何文档,它可能会涉及编译器在将函数结果作为参数传递时生成临时变量来保存中间结果的区域。考虑以下代码:

procedure UseInterface(foo: IInterface);
begin
end;

procedure Test()
begin
    UseInterface(Create());
end;

编译器必须创建一个隐式临时变量来保存Create的结果,以便在它被传递到UseInterface时,确保接口的生命周期>=UseInterface调用的生命周期。这个隐式的临时变量将在拥有它的过程结束时被处理,即在Test()过程结束时。

你的指针赋值情况可能会落入与将中间接口值作为函数参数传递相同的情况,因为编译器无法“看到”该值的去处。

我记得这个领域已经有一些错误了。很久以前(D3?D4?),编译器根本没有对中间值进行计数。大多数时候它能正常工作,但在参数别名情况下会遇到麻烦。一旦解决了这个问题,就会有关于const参数的后续处理,我相信。一直以来都希望将中间值接口的处理移动到尽可能靠近需要它的语句之后,但我认为Win32优化器中从未实现这一点,因为编译器不适合处理语句或块级别的处理。


0

你无法保证编译器不会决定创建一个临时的不可见变量。

即使你这样做了,关闭优化(甚至堆栈帧?)可能会破坏你完美检查的代码。

即使你设法在所有可能的项目选项组合下审查你的代码 - 在类似Lazarus或者新的Delphi版本下编译你的代码也会让事情变得很麻烦。

最好的方法是使用“内部变量不能超出例程”的规则。我们通常不知道编译器是否会创建一些内部变量,但我们知道,任何这样的变量(如果被创建)将在例程结束时被终止。

因此,如果你有这样的代码:

// 1. Some code which may (or may not) create invisible variables
// 2. Some code which requires release of reference-counted data

E.g.:

Lib := LoadLibrary(Lib, 'xyz');
try
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- May be not OK
end;

然后你应该将“与接口一起工作”的代码块封装到子程序中:

procedure Work(const Lib: HModule);
begin
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
end; // <- Releases hidden variables (if any exist)

Lib := LoadLibrary(Lib, 'xyz');
try
  Work(Lib);
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- OK!
end;

这是一个简单但有效的规则。


在我的情况下,I:= CreateInterfaceFromLib(...) 导致了一个隐式局部变量。所以你建议的方法不会有帮助。无论如何,我已经在问题中清楚地展示了一种解决方法。这个解决方法基于隐式局部变量的生命周期受函数作用域控制。我的问题涉及到会导致隐式局部变量出现的情况。 - David Heffernan
我的观点是,首先问这个问题本身就是错误的。 - Alex
1
欢迎你持有那个观点,但你应该将它表达为评论。在代码中添加试图(不成功地)复制问题的解决方法,对我来说似乎很奇怪。 - David Heffernan

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