隐式接口variables的编译器处理是否logging?
不久前,我问了一个关于隐式接口variables的类似问题 。
这个问题的来源是我的代码中的一个错误,因为我没有意识到编译器创build的隐式接口variables的存在。 这个variables在拥有它的过程完成时已经完成。 这反过来造成了一个错误,由于variables的寿命比我预期的更长。
现在,我有一个简单的项目来说明编译器的一些有趣的行为:
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
按照您的想象进行编译。 局部variablesI
(函数的结果)作为隐含的var
parameter passing给Create
。 整理StoreToLocal
可以调用IntfClear
。 那里没有惊喜。
但是, StoreViaPointerToLocal
的处理方式不同。 编译器创build一个隐式局部variables,并将其传递给Create
。 Create
返回时,将执行对P^
的分配。 这留下了例程与两个本地variables保持对接口的引用。 整理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
我可以猜测为什么编译器正在这样做。 当它可以certificate赋值给结果variables不会引发exception(即,如果variables是本地variables),则直接使用结果variables。 否则,它使用一个隐式本地,并在函数返回后复制接口,从而确保在发生exception时不泄漏引用。
但是在文档中我找不到任何这样的声明。 这很重要,因为界面的生命周期很重要,作为程序员,你需要有时候能够影响它。
那么,有没有人知道是否有这种行为的文件? 如果没有人有更多的知识呢? 如何处理实例字段,我还没有检查。 当然,我可以为自己尝试一切,但我正在寻找更正式的声明,并始终宁愿避免依靠试验和错误的实施细节。
更新1
为了回答雷米的问题,当我需要在界面背后敲定对象之前,对于我来说这很重要。
begin AcquirePythonGIL; try PyObject := CreatePythonObject; try //do stuff with PyObject finally Finalize(PyObject); end; finally ReleasePythonGIL; end; end;
正如这样写的,这很好。 但是在真正的代码中,我有第二个隐含的本地,在GIL被释放和被轰炸后最终确定。 我通过将Acquire / Release GIL中的代码解压缩成一个单独的方法来解决这个问题,从而缩小了接口variables的范围。
如果有这种行为的任何文档,它可能会在编译器生产临时variables的区域,以传递函数结果作为参数时保留中间结果。 考虑这个代码:
procedure UseInterface(foo: IInterface); begin end; procedure Test() begin UseInterface(Create()); end;
编译器必须创build一个隐式的临时variables来保存Create的结果,因为它传递给了UseInterface,以确保接口的生命周期> = UseInterface调用的生命周期。 这个隐含的临时variables将被放置在拥有它的过程的末尾,在这种情况下,在Test()过程的末尾。
由于编译器无法“看到”值的去向,因此指针赋值的情况可能与传递中间接口值作为函数参数一样存在于同一个桶中。
我记得这几年这个领域出现了一些问题。 很久以前(D3?D4?),编译器根本没有引用统计中间值。 它大部分时间工作,但在参数别名的情况下遇到麻烦。 一旦解决了这个问题,我相信这是一个关于const params的后续。 总是希望在需要的语句之后尽快地处理中间值接口,但是我不认为在Win32优化器中实现了,因为编译器没有设置以声明或块的粒度处理处置。
你不能保证编译器不会决定创build一个临时的不可见variables。
即使你这样做,closures优化(甚至是堆栈帧)可能会搞砸你的完美检查代码。
即使你设法在所有可能的项目组合选项下复习你的代码 – 编译你的代码,像Lazarus,甚至新的Delphi版本,都会带回地狱。
最好的办法是使用“内部variables不能超越常规”的规则。 我们通常不知道,如果编译器会创build一些内部variables,但是我们知道任何这样的variables(如果创build的话)会在例程存在的时候完成。
因此,如果你有这样的代码:
// 1. Some code which may (or may not) create invisible variables // 2. Some code which requires release of reference-counted data
例如:
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;
这是一个简单而有效的规则。