Ist die Compiler-Behandlung impliziter Schnittstellenvariablen dokumentiert?

86

Ich habe vor nicht allzu langer Zeit eine ähnliche Frage zu impliziten Schnittstellenvariablen gestellt.

Die Quelle dieser Frage war ein Fehler in meinem Code, weil ich nicht wusste, dass eine implizite Schnittstellenvariable vorhanden ist, die vom Compiler erstellt wurde. Diese Variable wurde abgeschlossen, als die Prozedur, der sie gehörte, abgeschlossen war. Dies verursachte wiederum einen Fehler, da die Lebensdauer der Variablen länger war als erwartet.

Jetzt habe ich ein einfaches Projekt, um ein interessantes Verhalten des Compilers zu veranschaulichen:

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.

StoreToLocalwird so zusammengestellt, wie Sie es sich vorstellen können. Die lokale Variable I, das Ergebnis der Funktion, wird als impliziter varParameter an übergeben Create. Das Aufräumen StoreToLocalführt zu einem einzigen Anruf bei IntfClear. Keine Überraschungen da.

Jedoch StoreViaPointerToLocalwird anders behandelt. Der Compiler erstellt eine implizite lokale Variable, an die er übergeben wird Create. Bei der CreateRückkehr wird die Zuordnung zu P^ausgeführt. Dadurch bleiben der Routine zwei lokale Variablen übrig, die Verweise auf die Schnittstelle enthalten. Das Aufräumen StoreViaPointerToLocalführt zu zwei Anrufen bei IntfClear.

Der kompilierte Code für StoreViaPointerToLocallautet wie folgt:

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 

Ich kann mir vorstellen, warum der Compiler dies tut. Wenn nachgewiesen werden kann, dass die Zuweisung zur Ergebnisvariablen keine Ausnahme auslöst (dh wenn die Variable lokal ist), wird die Ergebnisvariable direkt verwendet. Andernfalls wird ein implizites lokales Element verwendet und die Schnittstelle kopiert, sobald die Funktion zurückgegeben wurde. Auf diese Weise wird sichergestellt, dass die Referenz im Falle einer Ausnahme nicht verloren geht.

Aber ich kann keine Aussage dazu in der Dokumentation finden. Dies ist wichtig, da die Lebensdauer der Benutzeroberfläche wichtig ist und Sie als Programmierer gelegentlich Einfluss darauf nehmen müssen.

Weiß jemand, ob es eine Dokumentation dieses Verhaltens gibt? Wenn nicht, hat jemand mehr Wissen darüber? Wie mit Instanzfeldern umgegangen wird, habe ich noch nicht überprüft. Natürlich könnte ich alles selbst ausprobieren, aber ich suche nach einer formelleren Aussage und ziehe es immer vor, mich nicht auf Implementierungsdetails zu verlassen, die durch Ausprobieren ausgearbeitet wurden.

Update 1

Um Remys Frage zu beantworten, war es mir wichtig, wann ich das Objekt hinter der Schnittstelle finalisieren musste, bevor ich eine weitere Finalisierung durchführte.

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

Wie so geschrieben ist es in Ordnung. Aber im realen Code hatte ich einen zweiten impliziten lokalen, der abgeschlossen wurde, nachdem die GIL veröffentlicht und bombardiert wurde. Ich habe das Problem gelöst, indem ich den Code in der Acquire / Release GIL in eine separate Methode extrahiert und damit den Umfang der Schnittstellenvariablen eingeschränkt habe.

David Heffernan
quelle
8
Ich weiß nicht, warum dies abgelehnt wurde, ansonsten ist die Frage wirklich komplex. Upvoted für weit über meinem Kopf zu sein. Ich weiß, dass genau dieses Stück Arcanum in einer App, an der ich vor einem Jahr gearbeitet habe, zu subtilen Fehlern beim Zählen von Referenzen geführt hat. Einer unserer besten Geeks hat Stunden damit verbracht, es herauszufinden. Am Ende haben wir es umgangen, aber nie verstanden, wie der Compiler funktionieren sollte.
Warren P
3
@Serg Der Compiler hat seine Referenzzählung perfekt durchgeführt. Das Problem war, dass es eine zusätzliche Variable gab, die eine Referenz enthielt, die ich nicht sehen konnte. Was ich wissen möchte, ist, was den Compiler dazu veranlasst, eine solche zusätzliche, versteckte Referenz zu verwenden.
David Heffernan
3
Ich verstehe Sie, aber eine gute Praxis ist es, Code zu schreiben, der nicht von solchen zusätzlichen Variablen abhängt. Lassen Sie den Compiler diese Variablen so oft erstellen, wie er möchte. Ein fester Code sollte nicht davon abhängen.
Kludg
2
Ein weiteres Beispiel, wenn dies geschieht:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle
2
Ich bin versucht, dies als Compiler-Fehler zu bezeichnen ... Temporäre sollten gelöscht werden, nachdem sie den Gültigkeitsbereich verlassen haben. Dies sollte das Ende der Zuweisung (und nicht das Ende der Funktion) sein. Wenn Sie dies nicht tun, entstehen subtile Fehler, wie Sie festgestellt haben.
Nneonneo

Antworten:

15

Wenn es eine Dokumentation dieses Verhaltens gibt, liegt es wahrscheinlich im Bereich der Compilerproduktion temporärer Variablen, Zwischenergebnisse zu speichern, wenn Funktionsergebnisse als Parameter übergeben werden. Betrachten Sie diesen Code:

procedure UseInterface(foo: IInterface);
begin
end;

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

Der Compiler muss eine implizite temporäre Variable erstellen, um das Ergebnis von Create zu speichern, wenn es an UseInterface übergeben wird, um sicherzustellen, dass die Schnittstelle eine Lebensdauer> = die Lebensdauer des UseInterface-Aufrufs hat. Diese implizite temporäre Variable wird am Ende der Prozedur, deren Eigentümer sie ist, entsorgt, in diesem Fall am Ende der Test () -Prozedur.

Es ist möglich, dass Ihr Zeigerzuweisungsfall in den gleichen Bereich fällt wie das Übergeben von Zwischenschnittstellenwerten als Funktionsparameter, da der Compiler nicht "sehen" kann, wohin der Wert geht.

Ich erinnere mich, dass es im Laufe der Jahre einige Fehler in diesem Bereich gegeben hat. Vor langer Zeit (D3? D4?) Hat der Compiler den Zwischenwert überhaupt nicht referenziert. Es hat die meiste Zeit funktioniert, ist aber in Parameter-Alias-Situationen in Schwierigkeiten geraten. Nachdem dies angesprochen wurde, gab es meines Erachtens ein Follow-up zu const params. Es bestand immer der Wunsch, die Entsorgung der Zwischenwertschnittstelle so bald wie möglich nach der Anweisung zu verschieben, in der sie benötigt wurde, aber ich glaube nicht, dass dies jemals im Win32-Optimierer implementiert wurde, da der Compiler einfach nicht festgelegt war zur Entsorgung bei Anweisung oder Blockgranularität.

dthorpe
quelle
0

Sie können nicht garantieren, dass der Compiler nicht entscheidet, eine zeitlich unsichtbare Variable zu erstellen.

Und selbst wenn Sie dies tun, kann die deaktivierte Optimierung (oder sogar Stapelrahmen?) Ihren perfekt überprüften Code durcheinander bringen.

Und selbst wenn Sie es schaffen, Ihren Code unter allen möglichen Kombinationen von Projektoptionen zu überprüfen, bringt das Kompilieren Ihres Codes unter Lazarus oder sogar einer neuen Delphi-Version die Hölle zurück.

Am besten verwenden Sie die Regel "Interne Variablen können Routine nicht überleben". Wir wissen normalerweise nicht, ob der Compiler einige interne Variablen erstellen würde oder nicht, aber wir wissen, dass solche Variablen (falls erstellt) finalisiert werden, wenn Routine vorhanden ist.

Wenn Sie also folgenden Code haben:

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

Z.B:

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;

Dann sollten Sie einfach den Block "Mit Schnittstelle arbeiten" in das Unterprogramm einschließen:

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 ist eine einfache, aber wirksame Regel.

Alex
quelle
In meinem Szenario führte I: = CreateInterfaceFromLib (...) zu einem impliziten lokalen Ergebnis. Was Sie also vorschlagen, wird nicht helfen. Auf jeden Fall habe ich in der Frage bereits deutlich eine Problemumgehung aufgezeigt. Eine basierend auf der Lebensdauer impliziter Einheimischer, die vom Funktionsumfang gesteuert werden. Meine Frage betraf die Szenarien, die zu den impliziten Einheimischen führen würden.
David Heffernan
Mein Punkt war, dass dies eine falsche Frage ist.
Alex
1
Sie sind zu diesem Standpunkt willkommen, sollten ihn jedoch als Kommentar ausdrücken. Das Hinzufügen von Code, der (erfolglos) versucht, die Problemumgehungen der Frage zu reproduzieren, erscheint mir seltsam.
David Heffernan