Czy jest udokumentowane traktowanie przez kompilator niejawnych zmiennych interfejsu?

86

Nie tak dawno zadałem podobne pytanie dotyczące niejawnych zmiennych interfejsu.

Źródłem tego pytania był błąd w moim kodzie, ponieważ nie byłem świadomy istnienia niejawnej zmiennej interfejsu utworzonej przez kompilator. Ta zmienna została sfinalizowana, gdy procedura, która była jej właścicielem, zakończyła się. To z kolei spowodowało błąd, ponieważ czas życia zmiennej był dłuższy niż się spodziewałem.

Teraz mam prosty projekt ilustrujący kilka interesujących zachowań kompilatora:

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.

StoreToLocaljest kompilowany tak, jak można sobie wyobrazić. Zmienna lokalna I, wynik funkcji, jest przekazywana jako niejawny varparametr do Create. Porządkowanie StoreToLocalwyników w jednym telefonie do IntfClear. Żadnych niespodzianek.

Jednak StoreViaPointerToLocaljest traktowany inaczej. Kompilator tworzy niejawną zmienną lokalną, do której przekazuje Create. Kiedy Createzwraca, P^wykonywane jest przypisanie do . To pozostawia procedurę z dwiema zmiennymi lokalnymi przechowującymi odwołania do interfejsu. Porządkowanie StoreViaPointerToLocalwyników w dwóch wezwaniach IntfClear.

Skompilowany kod StoreViaPointerToLocalwygląda następująco:

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 

Mogę się domyślić, dlaczego kompilator to robi. Jeśli może udowodnić, że przypisanie do zmiennej wynikowej nie spowoduje wyjątku (tj. Jeśli zmienna jest lokalna), wówczas używa zmiennej wynikowej bezpośrednio. W przeciwnym razie używa niejawnego lokalnego i kopiuje interfejs po zwróceniu funkcji, zapewniając w ten sposób, że nie wyciekniemy odwołania w przypadku wyjątku.

Ale nie mogę znaleźć żadnego stwierdzenia na ten temat w dokumentacji. Ma to znaczenie, ponieważ żywotność interfejsu jest ważna i jako programista musisz mieć możliwość wpływania na niego od czasu do czasu.

Więc czy ktoś wie, czy istnieje dokumentacja tego zachowania? Jeśli nie, to czy ktoś ma o tym więcej wiedzy? Jak obsługiwane są pola instancji, jeszcze tego nie sprawdziłem. Oczywiście mógłbym wypróbować to wszystko samodzielnie, ale szukam bardziej formalnego oświadczenia i zawsze wolę unikać polegania na szczegółach implementacji opracowanych metodą prób i błędów.

Zaktualizuj 1

Odpowiadając na pytanie Remy'ego, ważne było dla mnie, kiedy musiałem sfinalizować obiekt za interfejsem przed wykonaniem kolejnej finalizacji.

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

Jak napisano w ten sposób, jest w porządku. Ale w prawdziwym kodzie miałem drugi domyślny lokalny, który został sfinalizowany po wydaniu GIL i zbombardowaniu. Rozwiązałem problem, wyodrębniając kod z GIL Acquire / Release do oddzielnej metody i tym samym zawęziłem zakres zmiennej interfejsu.

David Heffernan
źródło
8
Nie wiem, dlaczego głosowano negatywnie, poza tym, że pytanie jest naprawdę złożone. Głosowany za byciem daleko poza moją głową. Wiem, że właśnie ten kawałek arkana spowodował pewne subtelne błędy w liczeniu referencji w aplikacji, nad którą pracowałem rok temu. Jeden z naszych najlepszych maniaków spędził godziny, zastanawiając się nad tym. W końcu poradziliśmy sobie z tym, ale nigdy nie zrozumieliśmy, jak kompilator ma działać.
Warren P
3
@Serg Kompilator zliczał referencje doskonale. Problem polegał na tym, że istniała dodatkowa zmienna zawierająca odniesienie, którego nie mogłem zobaczyć. Chcę wiedzieć, co prowokuje kompilator do podjęcia takiego dodatkowego, ukrytego odniesienia.
David Heffernan
3
Rozumiem cię, ale dobrą praktyką jest pisanie kodu, który nie zależy od takich dodatkowych zmiennych. Niech kompilator utworzy te zmienne tak często, jak chce, solidny kod nie powinien na tym polegać.
kludg
2
Inny przykład, kiedy to się dzieje:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle,
2
Kusi mnie, aby nazwać to błędem kompilatora ... elementy tymczasowe powinny zostać wyczyszczone, gdy wyjdą poza zakres, co powinno być końcem przypisania (a nie końcem funkcji). Niezrobienie tego powoduje subtelne błędy, jak odkryłeś.
nneonneo

Odpowiedzi:

15

Jeśli istnieje jakakolwiek dokumentacja tego zachowania, prawdopodobnie będzie to obszar tworzenia przez kompilator zmiennych tymczasowych do przechowywania wyników pośrednich podczas przekazywania wyników funkcji jako parametrów. Rozważ ten kod:

procedure UseInterface(foo: IInterface);
begin
end;

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

Kompilator musi utworzyć niejawną zmienną tymczasową, aby przechowywać wynik Create, gdy jest on przekazywany do UseInterface, aby upewnić się, że interfejs ma okres istnienia> = czas życia wywołania UseInterface. Ta niejawna zmienna tymczasowa zostanie usunięta na końcu procedury, do której należy, w tym przypadku na końcu procedury Test ().

Możliwe, że przypadek przypisania wskaźnika może należeć do tego samego zasobnika, co przekazywanie wartości interfejsu pośredniego jako parametrów funkcji, ponieważ kompilator nie „widzi”, dokąd zmierza wartość.

Pamiętam, że na przestrzeni lat w tym obszarze było kilka błędów. Dawno temu (D3? D4?) Kompilator w ogóle nie liczył wartości pośredniej. Działało przez większość czasu, ale wpadało w kłopoty w sytuacjach aliasów parametrów. Wydaje mi się, że po tym, jak się tym zajęto, podjęto dalsze działania w zakresie parametrów konst. Zawsze istniała chęć przeniesienia usuwania interfejsu wartości pośredniej na tak szybko, jak to możliwe po instrukcji, w której był potrzebny, ale nie sądzę, aby kiedykolwiek został zaimplementowany w optymalizatorze Win32, ponieważ kompilator po prostu nie był ustawiony do obsługi dyspozycji na podstawie instrukcji lub bloku.

dthorpe
źródło
0

Nie można zagwarantować, że kompilator nie zdecyduje się na utworzenie tymczasowej niewidocznej zmiennej.

A nawet jeśli to zrobisz, wyłączona optymalizacja (lub nawet ramki stosu?) Może zepsuć twój doskonale sprawdzony kod.

I nawet jeśli uda Ci się przejrzeć swój kod pod wszystkimi możliwymi kombinacjami opcji projektu - kompilacja kodu w ramach czegoś takiego jak Lazarus lub nawet nowa wersja Delphi przyniesie piekło z powrotem.

Najlepszym rozwiązaniem byłoby użycie zasady „zmienne wewnętrzne nie mogą przetrwać rutyny”. Zwykle nie wiemy, czy kompilator utworzyłby jakieś wewnętrzne zmienne, czy nie, ale wiemy, że wszelkie takie zmienne (jeśli zostałyby utworzone) zostałyby sfinalizowane, gdy istnieje procedura.

Dlatego jeśli masz taki kod:

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

Na przykład:

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;

Następnie należy po prostu zawinąć blok „Praca z interfejsem” do podprogramu:

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;

To prosta, ale skuteczna zasada.

Alex
źródło
W moim scenariuszu I: = CreateInterfaceFromLib (...) spowodowało niejawny lokalny. Więc to, co sugerujesz, nie pomoże. W każdym razie pokazałem już jasno sposób obejścia tego pytania. Jeden oparty na okresie istnienia niejawnych lokalizacji lokalnych kontrolowanych przez zakres funkcji. Moje pytanie dotyczyło scenariuszy, które doprowadziłyby do ukrytych miejscowych.
David Heffernan,
Chodziło mi o to, że w pierwszej kolejności jest to niewłaściwe pytanie.
Alex
1
Zapraszamy do tego punktu widzenia, ale należy to wyrazić jako komentarz. Dodanie kodu, który próbuje (bezskutecznie) odtworzyć obejście tego pytania, wydaje mi się dziwne.
David Heffernan,