delphi - ¿El tratamiento del compilador de las variables de interfaz implícitas está documentado?
interface delphi-2010 (2)
Hice una question similar sobre variables de interfaz implícitas no hace mucho tiempo.
La fuente de esta pregunta fue un error en mi código debido a que no estaba al tanto de la existencia de una variable de interfaz implícita creada por el compilador. Esta variable se finalizó cuando finalizó el procedimiento que lo poseía. Esto a su vez causó un error debido a que la vida útil de la variable era más larga de lo que había anticipado.
Ahora, tengo un proyecto simple para ilustrar un comportamiento interesante del compilador:
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
se compila tal como se imaginaría. La variable local I
, el resultado de la función, se pasa como un parámetro var
implícito para Create
. La StoreToLocal
para StoreToLocal
da StoreToLocal
resultado una sola llamada a IntfClear
. No hay sorpresas allí.
Sin embargo, StoreViaPointerToLocal
se trata de manera diferente. El compilador crea una variable local implícita que pasa a Create
. Cuando Create
regresa, se realiza la asignación a P^
. Esto deja la rutina con dos variables locales que contienen referencias a la interfaz. La StoreViaPointerToLocal
para StoreViaPointerToLocal
da StoreViaPointerToLocal
resultado dos llamadas a IntfClear
.
El código compilado para StoreViaPointerToLocal
es así:
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
Puedo adivinar por qué el compilador está haciendo esto. Cuando puede demostrar que la asignación a la variable de resultado no generará una excepción (es decir, si la variable es local), utiliza la variable de resultado directamente. De lo contrario, utiliza un local implícito y copia la interfaz una vez que la función ha regresado, garantizando así que no se produzca la fuga de la referencia en caso de una excepción.
Pero no puedo encontrar ninguna declaración de esto en la documentación. Importa porque la vida útil de la interfaz es importante y, como programador, debe poder influenciarlo en ocasiones.
Entonces, ¿alguien sabe si hay alguna documentación de este comportamiento? Si no, ¿alguien tiene más conocimiento de eso? Cómo se manejan los campos de instancia, aún no lo he comprobado. Por supuesto que podría intentarlo por mí mismo, pero estoy buscando una declaración más formal y siempre prefiero evitar confiar en los detalles de implementación resueltos por prueba y error.
Actualización 1
Para responder la pregunta de Remy, me importaba cuando necesitaba finalizar el objeto detrás de la interfaz antes de llevar a cabo otra finalización.
begin
AcquirePythonGIL;
try
PyObject := CreatePythonObject;
try
//do stuff with PyObject
finally
Finalize(PyObject);
end;
finally
ReleasePythonGIL;
end;
end;
Como está escrito así, está bien. Pero en el código real tenía un segundo local implícito que se finalizó después de que se liberara el GIL y se bombardeara. Resolví el problema extrayendo el código dentro de Adquirir / Liberar GIL en un método separado y, de este modo, reduje el alcance de la variable de interfaz.
No se puede garantizar que el compilador no decida crear una variable temporal invisible.
E incluso si lo hace, la optimización desactivada (¿o incluso los marcos de la pila?) Puede estropear su código perfectamente verificado.
E incluso si logras revisar tu código bajo todas las combinaciones posibles de opciones de proyecto, compilar tu código en algo como Lazarus o incluso la nueva versión de Delphi te devolverá el infierno.
Una mejor opción sería usar la regla "las variables internas no pueden sobrevivir a la rutina". Por lo general, no sabemos si el compilador creará algunas variables internas o no, pero sí sabemos que esas variables (si se crearon) se finalizarán cuando exista la rutina.
Por lo tanto, si tienes un código como este:
// 1. Some code which may (or may not) create invisible variables
// 2. Some code which requires release of reference-counted data
P.ej:
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;
Luego, simplemente debe ajustar el bloque "Trabajar con interfaz" en la subrutina:
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;
Es una regla simple pero efectiva.
Si hay alguna documentación de este comportamiento, probablemente será en el área de producción de compilador de variables temporales para mantener los resultados intermedios al pasar los resultados de la función como parámetros. Considera este código:
procedure UseInterface(foo: IInterface);
begin
end;
procedure Test()
begin
UseInterface(Create());
end;
El compilador tiene que crear una variable de temperatura implícita para contener el resultado de Crear a medida que se pasa a UseInterface, para asegurarse de que la interfaz tenga una vida> = el tiempo de vida de la llamada UseInterface. Esa variable de temperatura implícita se eliminará al final del procedimiento que lo posee, en este caso, al final del procedimiento Test ().
Es posible que su caso de asignación de puntero pueda caer en la misma categoría que pasar los valores intermedios de la interfaz como parámetros de función, ya que el compilador no puede "ver" hacia dónde va el valor.
Recuerdo que ha habido algunos errores en esta área a lo largo de los años. Hace mucho tiempo (D3? D4?), El compilador no hizo referencia a contar el valor intermedio en absoluto. Funcionó la mayor parte del tiempo, pero se metió en problemas en situaciones de alias de parámetros. Una vez que se abordó, hubo un seguimiento de const params, creo. Siempre hubo un deseo de mover la eliminación de la interfaz de valor intermedio lo más pronto posible después de la declaración en la que se necesitaba, pero no creo que alguna vez se haya implementado en el optimizador de Win32 porque el compilador simplemente no estaba configurado. para la eliminación de manejo en la granularidad de declaración o bloque.