Задокументировано ли отношение компилятора к неявным интерфейсным переменным?

Я просил аналогичную question о неявных интерфейсных переменных не так давно.

Источником этого вопроса была ошибка в моем коде из-за того, что я не знал о существовании неявной переменной интерфейса, созданной компилятором. Эта переменная была завершена, когда процедура, в которой она была закончена. Это, в свою очередь, вызвало ошибку из-за того, что время жизни переменной было дольше, чем я ожидал.

Теперь у меня есть простой проект, чтобы проиллюстрировать интересное поведение компилятора:

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 компилируется так, как вы себе представляете. Локальная переменная I, результат функции, передается как неявный параметр var на Create. Приведение в порядок для StoreToLocal приводит к одному вызову IntfClear. Никаких сюрпризов нет.

Однако StoreViaPointerToLocal обрабатывается по-разному. Компилятор создает неявную локальную переменную, которая переходит на Create. Когда Create возвращается, выполняется присвоение P^. Это оставляет процедуру с двумя локальными переменными, содержащими ссылки на интерфейс. Приведение в порядок для StoreViaPointerToLocal приводит к двум вызовам IntfClear.

Скомпилированный код для StoreViaPointerToLocal выглядит следующим образом:

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 

Я могу догадаться, почему компилятор делает это. Когда это может доказать, что присвоение переменной результата не приведет к возникновению исключения (т.е. Если переменная является локальной), то она напрямую использует переменную результата. В противном случае он использует неявный локальный и копирует интерфейс после возвращения функции, тем самым гарантируя, что мы не будем утечка ссылки в случае исключения.

Но я не могу найти никаких утверждений об этом в документации. Это важно, потому что время жизни интерфейса важно, и как программист вам нужно иметь возможность влиять на него иногда.

Итак, кто-нибудь знает, есть ли какая-либо документация по этому поведению? Если никто не знает об этом больше? Как обрабатываются поля экземпляров, я еще не проверял это. Конечно, я мог бы попробовать все для себя, но я ищу более формальное выражение и всегда предпочитаю не полагаться на детали реализации, разработанные методом проб и ошибок.

Обновление 1

Чтобы ответить на вопрос Реми, мне важно, когда мне нужно было завершить работу над объектом за интерфейсом, прежде чем выполнять еще одну финализацию.

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

Как написано так, это нормально. Но в реальном коде у меня был второй неявный локальный, который был финализирован после того, как GIL был выпущен и был взломан. Я решил проблему, извлекая код внутри Acquire/Release GIL в отдельный метод и тем самым сузил область интерфейсной переменной.

Ответ 1

Если есть какая-либо документация по этому поведению, вероятно, будет в области создания компилятора временных переменных промежуточные результаты при передаче результатов функции в качестве параметров. Рассмотрим этот код:

procedure UseInterface(foo: IInterface);
begin
end;

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

Компилятор должен создать неявную временную переменную, чтобы удерживать результат Create, поскольку он передается в UseInterface, чтобы убедиться, что интерфейс имеет время жизни >= время жизни вызова UseInterface. Эта неявная временная переменная будет располагаться в конце процедуры, которой она владеет, в этом случае в конце процедуры Test().

Возможно, что ваш случай назначения указателя может попасть в тот же самый ведро, что и промежуточные значения интерфейса в качестве параметров функции, поскольку компилятор не может "видеть", где это значение идет.

Я помню, что за последние годы в этой области было несколько ошибок. Давным-давно (D3? D4?) Компилятор вообще не ссылался на среднее промежуточное значение. Он работал большую часть времени, но столкнулся с проблемами в ситуациях псевдонимов параметров. После того, как это было рассмотрено, я считаю, что в отношении const params наблюдалось продолжение. Всегда было желание перенести удаление интерфейса промежуточных значений до того, как это было возможно, после заявления, в котором оно было необходимо, но я не думаю, что это когда-либо было реализовано в оптимизаторе Win32, потому что компилятор просто не был установлен для обработки утилиты при выполнении операции или блочной детализации.

Ответ 2

Вы не можете гарантировать, что компилятор не решит создать временную невидимую переменную.

И даже если вы это сделаете, отключенная оптимизация (или даже стековые фреймы?) может испортить ваш отлично проверенный код.

И даже если вам удастся просмотреть ваш код под всеми возможными комбинациями параметров проекта - компиляция вашего кода под чем-то вроде Lazarus или даже новая версия Delphi вернет ад.

Лучше всего было бы использовать правило "внутренние переменные не могут пережить рутину". Обычно мы не знаем, если компилятор создаст некоторые внутренние переменные или нет, но мы знаем, что любые такие переменные (если они созданы) будут завершены, когда будет существовать обычная программа.

Поэтому, если у вас есть такой код:

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

например:.

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;

Затем вы должны просто поместить блок "Работа с интерфейсом" в подпрограмму:

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;

Это простое, но эффективное правило.