Il trattamento da parte del compilatore delle variabili implicite dell'interfaccia è documentato?


86

Ho posto una domanda simile sulle variabili di interfaccia implicite non molto tempo fa.

La fonte di questa domanda era un bug nel mio codice dovuto al fatto che non ero a conoscenza dell'esistenza di una variabile di interfaccia implicita creata dal compilatore. Questa variabile è stata finalizzata al termine della procedura che la possedeva. Questo a sua volta ha causato un bug dovuto al fatto che la durata della variabile era più lunga di quanto avevo previsto.

Ora, ho un semplice progetto per illustrare alcuni comportamenti interessanti del compilatore:

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è compilato proprio come potresti immaginare. La variabile locale I, il risultato della funzione, viene passata come varparametro implicito a Create. Il riordino dei StoreToLocalrisultati in una sola chiamata a IntfClear. Nessuna sorpresa lì.

Tuttavia, StoreViaPointerToLocalviene trattato in modo diverso. Il compilatore crea una variabile locale implicita a cui passa Create. Quando Createritorna, P^viene eseguita l'assegnazione a . Ciò lascia la routine con due variabili locali che contengono riferimenti all'interfaccia. Il riordino per StoreViaPointerToLocalrisultati in due chiamate a IntfClear.

Il codice compilato per StoreViaPointerToLocalè così:

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 

Posso immaginare il motivo per cui il compilatore sta facendo questo. Quando può provare che l'assegnazione alla variabile risultato non solleverà un'eccezione (cioè se la variabile è locale), allora usa direttamente la variabile risultato. Altrimenti utilizza un locale implicito e copia l'interfaccia una volta restituita la funzione, assicurando così che non perdiamo il riferimento in caso di eccezione.

Ma non riesco a trovare alcuna dichiarazione di questo nella documentazione. È importante perché la durata dell'interfaccia è importante e come programmatore devi essere in grado di influenzarla occasionalmente.

Quindi, qualcuno sa se esiste una documentazione di questo comportamento? Se no qualcuno ne ha più conoscenza? Come vengono gestiti i campi di istanza, non l'ho ancora verificato. Ovviamente potrei provare tutto da solo, ma cerco una dichiarazione più formale e preferisco sempre evitare di fare affidamento sui dettagli di implementazione elaborati per tentativi ed errori.

Aggiorna 1

Per rispondere alla domanda di Remy, mi importava quando avevo bisogno di finalizzare l'oggetto dietro l'interfaccia prima di eseguire un'altra finalizzazione.

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

Come scritto in questo modo va bene. Ma nel codice reale avevo un secondo locale implicito che è stato finalizzato dopo che il GIL è stato rilasciato e che è stato bombardato. Ho risolto il problema estraendo il codice all'interno del GIL Acquire / Release in un metodo separato e quindi ristretto l'ambito della variabile di interfaccia.


8
Non so perché questo sia stato svalutato, a parte questo la domanda è davvero complessa. Votato per essere andato oltre la mia testa. So che esattamente questo pezzo di arcano ha provocato alcuni sottili bug nel conteggio dei riferimenti in un'app su cui ho lavorato un anno fa. Uno dei nostri migliori smanettoni ha passato ore a capirlo. Alla fine ci abbiamo aggirato ma non abbiamo mai capito come doveva funzionare il compilatore.
Warren P

3
@Serg Il compilatore ha eseguito perfettamente il conteggio dei riferimenti. Il problema era che c'era una variabile extra che conteneva un riferimento che non potevo vedere. Quello che voglio sapere è cosa spinge il compilatore a prendere un riferimento così extra, nascosto.
David Heffernan

3
Ti capisco, ma una buona pratica è scrivere codice che non dipenda da tali variabili extra. Lascia che il compilatore crei queste variabili quanto vuole, un codice solido non dovrebbe dipendere da esso.
kludg

2
Un altro esempio in cui ciò sta accadendo:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle

2
Sono tentato di chiamarlo un bug del compilatore ... i provvisori dovrebbero essere cancellati dopo che sono usciti dall'ambito, che dovrebbe essere la fine dell'assegnazione (e non la fine della funzione). Non farlo produce errori sottili come hai scoperto.
nneonneo

Risposte:


15

Se esiste una documentazione di questo comportamento, sarà probabilmente nell'area della produzione del compilatore di variabili temporanee per contenere risultati intermedi quando si passano i risultati della funzione come parametri. Considera questo codice:

procedure UseInterface(foo: IInterface);
begin
end;

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

Il compilatore deve creare una variabile temporanea implicita per contenere il risultato di Create mentre viene passato a UseInterface, per assicurarsi che l'interfaccia abbia una durata> = la durata della chiamata UseInterface. Quella variabile temporanea implicita verrà eliminata alla fine della procedura che la possiede, in questo caso alla fine della procedura Test ().

È possibile che il caso dell'assegnazione del puntatore possa rientrare nello stesso bucket del passaggio di valori dell'interfaccia intermedi come parametri di funzione, poiché il compilatore non può "vedere" dove sta andando il valore.

Ricordo che negli anni ci sono stati alcuni bug in quest'area. Molto tempo fa (D3? D4?), Il compilatore non faceva riferimento al conteggio del valore intermedio. Ha funzionato per la maggior parte del tempo, ma ha avuto problemi in situazioni di alias dei parametri. Una volta risolto il problema, credo che ci sia stato un seguito per quanto riguarda i parametri const. C'era sempre il desiderio di spostare la disposizione dell'interfaccia del valore intermedio il prima possibile dopo l'istruzione in cui era necessaria, ma non credo che sia mai stato implementato nell'ottimizzatore Win32 perché il compilatore non era impostato per la gestione dello smaltimento a dichiarazione o blocco granularità.


0

Non puoi garantire che il compilatore non decida di creare una variabile invisibile temporale.

E anche se lo fai, l'ottimizzazione disattivata (o anche lo stack frame?) Potrebbe rovinare il tuo codice perfettamente controllato.

E anche se riesci a rivedere il tuo codice con tutte le possibili combinazioni di opzioni di progetto, compilare il tuo codice con qualcosa come Lazarus o anche una nuova versione di Delphi riporterà l'inferno.

Una soluzione migliore sarebbe usare la regola "le variabili interne non possono sopravvivere alla routine". Di solito non sappiamo se il compilatore creerebbe o meno alcune variabili interne, ma sappiamo che qualsiasi variabile di questo tipo (se creata) verrebbe finalizzata quando esiste la routine.

Pertanto, se hai un codice come questo:

// 1. Some code which may (or may not) create invisible variables
// 2. Some code which requires release of reference-counted data

Per esempio:

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;

Quindi dovresti semplicemente racchiudere il blocco "Lavora con l'interfaccia" nella 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;

È una regola semplice ma efficace.


Nel mio scenario, I: = CreateInterfaceFromLib (...) risultava in un locale implicito. Quindi quello che suggerisci non aiuta. In ogni caso, ho già dimostrato chiaramente una soluzione alternativa alla domanda. Uno basato sulla durata delle variabili locali implicite controllate dall'ambito della funzione. La mia domanda riguardava gli scenari che avrebbero portato alla gente del posto implicita.
David Heffernan

Il punto era che questa è una domanda sbagliata da porre in primo luogo.
Alex

1
Sei il benvenuto a quel punto di vista, ma dovresti esprimerlo come un commento. L'aggiunta di codice che tenta (senza successo) di riprodurre le soluzioni alternative della domanda, mi sembra strano.
David Heffernan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.