2011-10-13 17 views
83

Ho chiesto un simile question alle variabili di interfaccia implicite non molto tempo fa.È documentato il trattamento del compilatore delle variabili implicite dell'interfaccia?

L'origine di questa domanda era un bug nel mio codice a causa del 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 di proprietà. Questo a sua volta ha causato un bug a causa della durata della variabile che è più lunga di quanto avessi previsto.

Ora, Ho un progetto semplice per illustrare alcuni comportamenti interessanti dal 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 si potrebbe immaginare. La variabile locale I, il risultato della funzione, viene passata come parametro implicito var a Create. L'ordine per StoreToLocal produce una singola chiamata a IntfClear. Nessuna sorpresa lì.

Tuttavia, StoreViaPointerToLocal viene trattato in modo diverso. Il compilatore crea una variabile locale implicita che passa a Create. Quando viene restituito Create, viene eseguita l'assegnazione a P^. Questo lascia la routine con due variabili locali che contengono riferimenti all'interfaccia. L'ordine per StoreViaPointerToLocal comporta due chiamate allo IntfClear.

Il codice compilato per StoreViaPointerToLocal è come questo:

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ò dimostrare che l'assegnazione alla variabile risultato non genera un'eccezione (ad esempio se la variabile è un locale), utilizza direttamente la variabile risultato. Altrimenti utilizza un locale implicito e copia l'interfaccia una volta restituita la funzione, assicurando così che non ci siano perdite di riferimento in caso di eccezione.

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

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

Update 1

Per rispondere alla domanda di Remy, che contava per me quando avevo bisogno di finalizzare l'oggetto dietro l'interfaccia prima di effettuare 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 ho avuto un secondo locale implicito che è stato finalizzato dopo che il GIL è stato rilasciato e quello bombardato. Ho risolto il problema estraendo il codice all'interno di Acquire/Release GIL in un metodo separato e quindi ridotto l'ambito della variabile di interfaccia.

+7

Non so perché questo è stato downvoted, a parte che la domanda è davvero complessa. In aumento per essere al di sopra della mia testa. So che esattamente questo frammento di arcanum ha portato ad alcuni sottili bug di conteggio dei riferimenti in un'app che ho lavorato un anno fa. Uno dei nostri migliori geek ha passato ore a capirlo. Alla fine abbiamo lavorato attorno ad esso, ma non abbiamo mai capito come avrebbe dovuto funzionare il compilatore. –

+0

Quando si lavora con le interfacce di conteggio dei riferimenti, non si deve presumere che non ci siano altri client (il compilatore nel proprio caso) che hanno i propri riferimenti di interfaccia. Se tutti i client eseguono correttamente 'AddRef' /' Release', tutto dovrebbe funzionare correttamente. Altrimenti è un bug (il tuo bug dal momento che presumo che il compilatore faccia correttamente il conteggio dei riferimenti). – kludg

+3

@Serg Il compilatore ha fatto il suo conteggio di riferimento perfettamente. Il problema era che c'era una variabile extra con un riferimento che non potevo vedere. Quello che voglio sapere è ciò che provoca il compilatore a prendere un riferimento così extra, nascosto. –

risposta

13

Se esiste una documentazione di questo comportamento, sarà probabilmente nell'area di produzione del compilatore di variabili temporanee mantenere i risultati intermedi quando si passano i risultati della funzione come parametri.Considerate 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 Crea in quanto è passato nella UseInterface, per assicurarsi che l'interfaccia ha una durata> = la durata della chiamata UseInterface . Quella variabile temporanea implicita verrà eliminata alla fine della procedura che lo possiede, in questo caso alla fine della procedura Test().

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

Ricordo che ci sono stati alcuni bug in questo settore nel corso degli anni. Molto tempo fa (D3? D4?), Il compilatore non ha fatto riferimento a contare il valore intermedio. Ha funzionato la maggior parte del tempo, ma si è messo nei guai in situazioni di alias dei parametri. Una volta che è stato affrontato, credo che ci sia stato un follow up riguardo i params const. C'era sempre il desiderio di spostare la disposizione dell'interfaccia del valore intermedio fino al più presto possibile dopo la dichiarazione in cui era necessario, ma non penso che sia mai stata implementata nell'ottimizzatore Win32 perché il compilatore non era impostato per la gestione dello smaltimento a granularità di istruzioni o blocchi.

0

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

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

E anche se riesci a rivedere il tuo codice sotto tutte le possibili combinazioni di opzioni di progetto - la compilazione del tuo codice sotto qualcosa come Lazarus o anche la nuova versione di Delphi porterà indietro l'inferno.

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

Pertanto, se si dispone di codice come questo:

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

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; 

allora si dovrebbe semplicemente avvolgere "Lavora con interfaccia" blocco in 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.

+0

Nel mio scenario, I: = CreateInterfaceFromLib (...) si traduceva in un locale implicito. Quindi quello che suggerisci non aiuterà. In ogni caso, ho già dimostrato chiaramente una soluzione alternativa alla domanda. Uno basato sulla durata di locali impliciti controllati dall'ambito della funzione. La mia domanda riguardava gli scenari che avrebbero portato gli abitanti impliciti. –

+0

Il mio punto era che questa è una domanda sbagliata da chiedere in primo luogo. – Alex

+0

Sei il benvenuto in quel punto di vista ma dovresti esprimerlo come commento. Aggiungere codice che tenta (senza successo) di riprodurre i workaround della domanda, mi sembra strano. –

Problemi correlati