¿Está documentado el tratamiento del compilador de las variables de interfaz implícitas?

86

Hice una pregunta similar sobre las 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 trámite que la poseía. Esto, a su vez, provocó 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.

StoreToLocalse compila tal como se imagina. La variable local I, el resultado de la función, se pasa como varparámetro implícito a Create. El orden para StoreToLocalresultados en una sola llamada a IntfClear. No hay sorpresas ahí.

Sin embargo, StoreViaPointerToLocalse trata de manera diferente. El compilador crea una variable local implícita a la que pasa Create. Cuando Createregresa, se realiza la asignación a P^. Esto deja la rutina con dos variables locales que contienen referencias a la interfaz. El tidy up for StoreViaPointerToLocalda como resultado dos llamadas a IntfClear.

El código compilado para StoreViaPointerToLocales 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 probar que la asignación a la variable de resultado no generará una excepción (es decir, si la variable es local), entonces usa la variable de resultado directamente. De lo contrario, usa un local implícito y copia la interfaz una vez que la función ha regresado, lo que garantiza que no filtremos la referencia en caso de una excepción.

Pero no puedo encontrar ninguna declaración de esto en la documentación. Es importante porque la vida útil de la interfaz es importante y, como programador, debe poder influir en ella en ocasiones.

Entonces, ¿alguien sabe si hay alguna documentación de este comportamiento? Si no, ¿alguien tiene más conocimiento al respecto? ¿Cómo se manejan los campos de instancia? Aún no lo he verificado. Por supuesto, podría probarlo todo 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 a la pregunta de Remy, me importaba cuándo necesitaba finalizar el objeto detrás de la interfaz antes de realizar 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 lanzó el GIL y se bombardeó. Resolví el problema extrayendo el código dentro de Adquirir / Liberar GIL en un método separado y así estreché el alcance de la variable de interfaz.

David Heffernan
fuente
8
No sé por qué fue rechazado, aparte de que la pregunta es realmente compleja. Voto a favor por estar muy por encima de mi cabeza. Sé que exactamente este fragmento de arcano resultó en algunos errores sutiles de conteo de referencias en una aplicación en la que trabajé hace un año. Uno de nuestros mejores geeks pasó horas resolviéndolo. Al final, lo solucionamos, pero nunca entendimos cómo debía funcionar el compilador.
Warren P
3
@Serg El compilador hizo su referencia contando perfectamente. El problema era que había una variable adicional que contenía una referencia que no podía ver. Lo que quiero saber es qué provoca que el compilador tome una referencia extra, oculta.
David Heffernan
3
Te entiendo, pero una buena práctica es escribir código que no dependa de esas variables adicionales. Deje que el compilador cree estas variables tanto como quiera, un código sólido no debería depender de ello.
kludg
2
Otro ejemplo cuando esto está sucediendo:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle
2
Estoy tentado de llamar a esto un error del compilador ... los temporales deben borrarse después de que se salgan del alcance, que debe ser el final de la asignación (y no el final de la función). No hacerlo produce errores sutiles como ha descubierto.
nneonneo

Respuestas:

15

Si hay alguna documentación de este comportamiento, probablemente estará en el área de producción del compilador de variables temporales para contener resultados intermedios cuando se pasan resultados de funciones como parámetros. Considere este código:

procedure UseInterface(foo: IInterface);
begin
end;

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

El compilador tiene que crear una variable temporal implícita para contener el resultado de Create cuando se pasa a UseInterface, para asegurarse de que la interfaz tenga una vida útil> = la vida útil de la llamada a UseInterface. Esa variable temporal implícita se eliminará al final del procedimiento que la posee, en este caso al final del procedimiento Test ().

Es posible que su caso de asignación de puntero caiga en el mismo grupo que pasar valores de interfaz intermedios como parámetros de función, ya que el compilador no puede "ver" a 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 hacía referencia al recuento del 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ó eso, creo que hubo un seguimiento con respecto a los parámetros const. Siempre hubo un deseo de mover la eliminación de la interfaz de valor intermedio lo antes posible después de la declaración en la que se necesitaba, pero no creo que eso se haya implementado en el optimizador de Win32 porque el compilador simplemente no estaba configurado. listo para manipular la eliminación en la granularidad de la declaración o del bloque.

dthorpe
fuente
0

No 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 pila?) Puede estropear su código perfectamente verificado.

E incluso si logra revisar su código en todas las combinaciones posibles de opciones de proyecto, compilar su código en algo como Lazarus o incluso una nueva versión de Delphi traerá el infierno de vuelta.

Lo mejor sería utilizar la regla "las variables internas no pueden sobrevivir a la rutina". Por lo general, no sabemos si el compilador crearía algunas variables internas o no, pero sí sabemos que dichas variables (si se crean) se finalizarán cuando exista la rutina.

Por lo tanto, si tiene 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 envolver 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.

Alex
fuente
En mi escenario, yo: = CreateInterfaceFromLib (...) estaba dando como resultado un local implícito. Entonces, lo que sugieres no ayudará. En cualquier caso, ya demostré claramente una solución alternativa en la pregunta. Uno basado en la vida útil de los locales implícitos controlados por el alcance de la función. Mi pregunta se refería a los escenarios que llevarían a los locales implícitos.
David Heffernan
Mi punto fue que esta es una pregunta incorrecta para hacer en primer lugar.
Alex
1
Eres bienvenido a ese punto de vista, pero debes expresarlo como un comentario. Agregar código que intenta (sin éxito) reproducir las soluciones alternativas a la pregunta, me parece extraño.
David Heffernan