Le traitement par le compilateur des variables d'interface implicites est-il documenté?

86

J'ai posé une question similaire sur les variables d'interface implicites il n'y a pas si longtemps.

La source de cette question était un bogue dans mon code car je ne connaissais pas l'existence d'une variable d'interface implicite créée par le compilateur. Cette variable a été finalisée lorsque la procédure qui la possédait s'est terminée. Cela a à son tour provoqué un bug car la durée de vie de la variable était plus longue que ce que j'avais prévu.

Maintenant, j'ai un projet simple pour illustrer un comportement intéressant du compilateur:

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.

StoreToLocalest compilé comme vous l'imaginez. La variable locale I, le résultat de la fonction, est transmise en tant que varparamètre implicite à Create. Le rangement des StoreToLocalrésultats en un seul appel à IntfClear. Pas de surprises là-bas.

Cependant, StoreViaPointerToLocalest traité différemment. Le compilateur crée une variable locale implicite à laquelle il passe Create. Au Createretour, l'affectation à P^est effectuée. Cela laisse la routine avec deux variables locales contenant des références à l'interface. Le rangement des StoreViaPointerToLocalrésultats en deux appels à IntfClear.

Le code compilé pour StoreViaPointerToLocalest comme ceci:

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 

Je peux deviner pourquoi le compilateur fait cela. Lorsqu'il peut prouver que l'assignation à la variable de résultat ne lèvera pas d'exception (c'est-à-dire si la variable est locale), alors il utilise directement la variable de résultat. Sinon, il utilise un local implicite et copie l'interface une fois que la fonction est retournée, assurant ainsi que nous ne fuyons pas la référence en cas d'exception.

Mais je ne trouve aucune déclaration à ce sujet dans la documentation. C'est important car la durée de vie de l'interface est importante et en tant que programmeur, vous devez pouvoir l'influencer à l'occasion.

Alors, est-ce que quelqu'un sait s'il existe une documentation sur ce comportement? Sinon, est-ce que quelqu'un en a plus connaissance? Comment sont gérés les champs d'instance, je n'ai pas encore vérifié cela. Bien sûr, je pourrais tout essayer par moi-même, mais je recherche une déclaration plus formelle et je préfère toujours éviter de me fier aux détails d'implémentation élaborés par essais et erreurs.

Mise à jour 1

Pour répondre à la question de Remy, cela m'importait quand j'avais besoin de finaliser l'objet derrière l'interface avant de procéder à une autre finalisation.

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

Comme écrit comme ça, c'est bien. Mais dans le vrai code, j'avais un deuxième local implicite qui a été finalisé après la libération du GIL et qui a été bombardé. J'ai résolu le problème en extrayant le code à l'intérieur du GIL Acquire / Release dans une méthode distincte et j'ai donc réduit la portée de la variable d'interface.

David Heffernan
la source
8
Je ne sais pas pourquoi cela a été rejeté, à part cela, la question est vraiment complexe. J'ai voté pour être bien au-dessus de ma tête. Je sais qu'exactement ce morceau d'arcane a entraîné de subtils bogues de comptage de références dans une application sur laquelle j'ai travaillé il y a un an. L'un de nos meilleurs geeks a passé des heures à le découvrir. En fin de compte, nous avons contourné ce problème, mais nous n'avons jamais compris comment le compilateur devait fonctionner.
Warren P
3
@Serg Le compilateur a parfaitement fait son comptage de références. Le problème était qu'il y avait une variable supplémentaire contenant une référence que je ne pouvais pas voir. Ce que je veux savoir, c'est ce qui pousse le compilateur à prendre une telle référence cachée supplémentaire.
David Heffernan
3
Je vous comprends, mais une bonne pratique consiste à écrire du code qui ne dépend pas de ces variables supplémentaires. Laissez le compilateur créer ces variables autant qu'il le souhaite, un code solide ne devrait pas en dépendre.
kludg
2
Un autre exemple quand cela se produit:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle
2
Je suis tenté d'appeler cela un bogue du compilateur ... les temporaires devraient être effacées une fois qu'elles sont hors de portée, ce qui devrait être la fin de l'affectation (et non la fin de la fonction). Ne pas le faire produit des erreurs subtiles comme vous l'avez découvert.
nneonneo le

Réponses:

15

S'il existe une documentation sur ce comportement, ce sera probablement dans le domaine de la production par le compilateur de variables temporaires pour contenir les résultats intermédiaires lors du passage des résultats de fonction en tant que paramètres. Considérez ce code:

procedure UseInterface(foo: IInterface);
begin
end;

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

Le compilateur doit créer une variable temporaire implicite pour contenir le résultat de Create tel qu'il est passé dans UseInterface, pour s'assurer que l'interface a une durée de vie> = la durée de vie de l'appel UseInterface. Cette variable temporaire implicite sera supprimée à la fin de la procédure qui la possède, dans ce cas à la fin de la procédure Test ().

Il est possible que votre cas d'affectation de pointeur tombe dans le même compartiment que le passage de valeurs d'interface intermédiaires en tant que paramètres de fonction, car le compilateur ne peut pas "voir" où va la valeur.

Je me souviens qu'il y a eu quelques bugs dans ce domaine au fil des ans. Il y a longtemps (D3? D4?), Le compilateur ne faisait pas du tout référence à compter la valeur intermédiaire. Cela a fonctionné la plupart du temps, mais a eu des problèmes dans les situations d'alias de paramètres. Une fois que cela a été réglé, il y a eu un suivi concernant les paramètres const, je crois. Il y avait toujours un désir de déplacer la suppression de l'interface de valeur intermédiaire le plus tôt possible après l'instruction dans laquelle elle était nécessaire, mais je ne pense pas que cela ait jamais été implémenté dans l'optimiseur Win32 parce que le compilateur n'était tout simplement pas défini pour la manipulation de l'élimination à l'état ou la granularité de bloc.

dthorpe
la source
0

Vous ne pouvez pas garantir que le compilateur ne décidera pas de créer une variable invisible temporelle.

Et même si vous le faites, l'optimisation désactivée (ou même la pile de cadres?) Peut gâcher votre code parfaitement vérifié.

Et même si vous parvenez à revoir votre code sous toutes les combinaisons possibles d'options de projet - compiler votre code sous quelque chose comme Lazarus ou même une nouvelle version de Delphi ramènera l'enfer.

Un meilleur pari serait d'utiliser la règle «les variables internes ne peuvent pas survivre à la routine». Nous ne savons généralement pas, si le compilateur créerait des variables internes ou non, mais nous savons que de telles variables (si elles sont créées) seraient finalisées lorsque la routine existe.

Par conséquent, si vous avez un code comme celui-ci:

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

Par exemple:

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;

Ensuite, vous devez simplement envelopper le bloc "Travailler avec l'interface" dans le sous-programme:

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;

C'est une règle simple mais efficace.

Alex
la source
Dans mon scénario, I: = CreateInterfaceFromLib (...) entraînait un local implicite. Donc, ce que vous suggérez n'aidera pas. Dans tous les cas, j'ai déjà clairement démontré une solution de contournement dans la question. Un basé sur la durée de vie des locaux implicites contrôlés par la portée de la fonction. Ma question concernait les scénarios qui conduiraient aux locaux implicites.
David Heffernan
Mon point était que c'est une mauvaise question à poser en premier lieu.
Alex
1
Vous êtes les bienvenus à ce point de vue, mais vous devez l'exprimer sous forme de commentaire. L'ajout de code qui tente (sans succès) de reproduire les solutions de contournement de la question me semble étrange.
David Heffernan