编译器对隐式接口变量的处理是否有记录?
我问了一个类似的关于隐式接口变量的问题很久以前了。
这个问题的根源是我的代码中的一个错误,因为我不知道编译器创建的隐式接口变量的存在。当拥有该变量的过程完成时,该变量就被最终确定。由于变量的生命周期比我预期的要长,这又导致了一个错误。
现在,我有一个简单的项目来说明编译器的一些有趣行为:
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
我可以猜测编译器为什么这样做。当它可以证明分配给结果变量不会引发异常(即,如果该变量是本地变量)时,它会直接使用结果变量。否则,它使用隐式本地并在函数返回后复制接口,从而确保在发生异常时不会泄漏引用。
但我在文档中找不到任何对此的说明。这很重要,因为接口生命周期很重要,作为程序员,您有时需要能够影响它。
那么,有人知道是否有这种行为的任何记录吗?如果没有的话,有人有更多的了解吗?实例字段是如何处理的,我还没有检查过。当然,我可以自己尝试一下,但我正在寻找更正式的声明,并且总是倾向于避免依赖通过反复试验得出的实现细节。
更新 1
为了回答 Remy 的问题,当我需要在执行另一次终结之前终结界面后面的对象时,这对我来说很重要。
begin
AcquirePythonGIL;
try
PyObject := CreatePythonObject;
try
//do stuff with PyObject
finally
Finalize(PyObject);
end;
finally
ReleasePythonGIL;
end;
end;
像这样写就可以了。但在真实的代码中,我有第二个隐式本地,它是在 GIL 发布并被轰炸后最终确定的。我通过将 Acquire/Release GIL 内的代码提取到一个单独的方法中来解决这个问题,从而缩小了接口变量的范围。
I asked a similar question about implicit interface variables not so long ago.
The source of this question was a bug in my code due to me not being aware of the existence of an implicit interface variable created by the compiler. This variable was finalized when the procedure that owned it finished. This in turn caused a bug due to the lifetime of the variable being longer than I had anticipated.
Now, I have a simple project to illustrate some interesting behaviour from the compiler:
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
is compiled just as you would imagine. The local variable I
, the function's result, is passed as an implicit var
parameter to Create
. The tidy up for StoreToLocal
results in a single call to IntfClear
. No surprises there.
However, StoreViaPointerToLocal
is treated differently. The compiler creates an implicit local variable which it passes to Create
. When Create
returns, the assignment to P^
is performed. This leaves the routine with two local variables holding references to the interface. The tidy up for StoreViaPointerToLocal
results in two calls to IntfClear
.
The compiled code for StoreViaPointerToLocal
is like this:
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
I can guess as to why the compiler is doing this. When it can prove that assigning to the result variable will not raise an exception (i.e. if the variable is a local) then it uses the result variable directly. Otherwise it uses an implicit local and copies the interface once the function has returned thus ensuring that we don't leak the reference in case of an exception.
But I cannot find any statement of this in the documentation. It matters because interface lifetime is important and as a programmer you need to be able to influence it on occasion.
So, does anybody know if there is any documentation of this behaviour? If not does anyone have any more knowledge of it? How are instance fields handled, I have not checked that yet. Of course I could try it all out for myself but I'm looking for a more formal statement and always prefer to avoid relying on implementation detail worked out by trial and error.
Update 1
To answer Remy's question, it mattered to me when I needed to finalize the object behind the interface before carrying out another finalization.
begin
AcquirePythonGIL;
try
PyObject := CreatePythonObject;
try
//do stuff with PyObject
finally
Finalize(PyObject);
end;
finally
ReleasePythonGIL;
end;
end;
As written like this it is fine. But in the real code I had a second implicit local which was finalized after the GIL was released and that bombed. I solved the problem by extracting the code inside the Acquire/Release GIL into a separate method and thus narrowed the scope of the interface variable.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
如果有任何关于此行为的文档,则可能是在编译器生成临时变量的区域中,以在将函数结果作为参数传递时保存中间结果。考虑以下代码:
编译器必须创建一个隐式临时变量来保存 Create 的结果,因为它被传递到 UseInterface,以确保接口的生命周期 >= UseInterface 调用的生命周期。该隐式临时变量将在拥有它的过程的末尾进行处理,在本例中是在 Test() 过程的末尾。
您的指针赋值情况可能与将中间接口值作为函数参数传递相同,因为编译器无法“查看”该值的去向。
我记得这些年来这个领域出现了一些错误。很久以前(D3?D4?),编译器根本没有对中间值进行引用计数。它在大多数情况下都有效,但在参数别名情况下遇到了麻烦。我相信,一旦解决了这个问题,就会有关于 const params 的后续行动。总是希望将中间值接口的处理尽快移到需要它的语句之后,但我不认为在 Win32 优化器中实现了这一点,因为编译器尚未设置用于以语句或块粒度进行处理处置。
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:
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.
您不能保证编译器不会决定创建临时不可见变量。
即使您这样做,关闭的优化(甚至堆栈帧?)也可能会弄乱您完美检查的代码。
即使您设法在项目选项的所有可能组合下审查您的代码 - 在 Lazarus 甚至新的 Delphi 版本之类的东西下编译您的代码也会带来地狱。
最好的选择是使用“内部变量不能比常规”规则。我们通常不知道编译器是否会创建一些内部变量,但我们确实知道,当例程存在时,任何此类变量(如果创建)都会被最终确定。
因此,如果您有这样的代码:
例如:
那么您应该将“使用接口”块包装到子例程中:
这是一个简单但有效的规则。
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:
E.g.:
Then you should just wrap "Work with interface" block into subroutine:
It is a simple, but effective rule.