是否记录了隐式接口变量的编译器处理?

不久前,我问过一个类似的关于隐式接口变量的 有个问题

这个问题的根源是我的代码中的一个 bug,因为我没有意识到编译器创建的隐式接口变量的存在。当拥有该变量的过程结束时,该变量被最终确定。这反过来又导致了一个 bug,因为变量的生存期比我预期的要长。

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

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(函数的结果)作为隐式 var参数传递给 Create。对 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

我能猜到为什么编译器要这样做。当它能够证明赋值给 result 变量不会引发异常时(例如,如果变量是局部的) ,它就会直接使用 result 变量。否则,它会使用一个隐式本地函数,并在函数返回时复制接口,从而确保在出现异常时不会泄漏引用。

但我在文件中找不到任何这方面的陈述。这很重要,因为接口生命周期很重要,作为一个程序员,您需要能够偶尔影响它。

那么,有人知道这种行为是否有任何记录吗?如果没有的话,还有人知道吗?如何处理实例字段,我还没有检查。当然,我可以自己尝试所有这些,但我正在寻找一个更正式的声明,总是更喜欢避免依赖于实现细节的试验和错误。

更新1

要回答 Remy 的问题,当我需要在执行另一个终结之前终结接口后面的对象时,这对我很重要。

begin
AcquirePythonGIL;
try
PyObject := CreatePythonObject;
try
//do stuff with PyObject
finally
Finalize(PyObject);
end;
finally
ReleasePythonGIL;
end;
end;

像这样写就很好。但在真正的代码中,我有第二个隐式局部,这是在 GIL 发布后最终完成的,结果失败了。我解决这个问题的方法是将获取/发布 GIL 中的代码提取到一个单独的方法中,从而缩小了接口变量的范围。

2737 次浏览

If there is any documentation of this behavior, it will probably be in the area of compiler production of temporary variables to hold intermediate results when passing function results as parameters. Consider this code:

procedure UseInterface(foo: IInterface);
begin
end;


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

The compiler has to create an implicit temp variable to hold the result of Create as it is passed into UseInterface, to make sure that the interface has a lifetime >= the lifetime of the UseInterface call. That implicit temp variable will be disposed at the end of the procedure that owns it, in this case at the end of the Test() procedure.

It's possible that your pointer assignment case may fall into the same bucket as passing intermediate interface values as function parameters, since the compiler can't "see" where the value is going.

I recall there have been a few bugs in this area over the years. Long ago (D3? D4?), the compiler didn't reference count the intermediate value at all. It worked most of the time, but got into trouble in parameter alias situations. Once that was addressed there was a follow up regarding const params, I believe. There was always a desire to move disposal of the intermediate value interface up to as soon as possible after the statement in which it was needed, but I don't think that ever got implemented in the Win32 optimizer because the compiler just wasn't set up for handling disposal at statement or block granularity.

You can not guarantee that compiler will not decide to create a temporal invisible variable.

And even if you do, the turned off optimization (or even stack frames?) may mess up your perfectly checked code.

And even if you manage to review your code under all possible combinations of project options - compiling your code under something like Lazarus or even new Delphi version will bring hell back.

A best bet would be to use "internal variables can not outlive routine" rule. We usually do not know, if compiler would create some internal variables or not, but we do know, that any such variables (if created) would be finalized when routine exists.

Therefore, if you have code like this:

// 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;

Then you should just wrap "Work with interface" block into subroutine:

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;

It is a simple, but effective rule.