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

86

Не так давно я задал аналогичный вопрос о неявных переменных интерфейса.

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

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

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 был выпущен и провалился. Я решил проблему, выделив код внутри GIL Acquire / Release в отдельный метод и таким образом сузив область действия переменной интерфейса.

Дэвид Хеффернан
источник
8
Не знаю, почему это было отклонено, кроме того, что вопрос действительно сложный. Проголосовал за то, что был выше моей головы. Я действительно знаю, что именно этот кусок аркана привел к некоторым тонким ошибкам подсчета ссылок в приложении, над которым я работал год назад. Один из наших лучших компьютерных фанатов часами разбирался в этом. В конце концов, мы работали над этим, но так и не поняли, как должен работать компилятор.
Уоррен П.
3
@Serg Компилятор отлично подсчитал количество ссылок. Проблема заключалась в том, что была дополнительная переменная, содержащая ссылку, которую я не мог видеть. Я хочу знать, что побуждает компилятор использовать такую ​​дополнительную скрытую ссылку.
Дэвид Хеффернан
3
Я вас понимаю, но хорошая практика - писать код, который не зависит от таких дополнительных переменных. Пусть компилятор создает эти переменные сколько угодно, цельный код не должен от этого зависеть.
kludg
2
Другой пример, когда это происходит:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ондрей Келле
2
У меня есть соблазн назвать это ошибкой компилятора ... временные объекты должны быть очищены после того, как они выходят за пределы области видимости, что должно быть концом назначения (а не концом функции). Как вы обнаружили, несоблюдение этого правила приводит к неявным ошибкам.
nneonneo

Ответы:

15

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

procedure UseInterface(foo: IInterface);
begin
end;

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

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

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

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

Dthorpe
источник
0

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

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

И даже если вам удастся просмотреть свой код при всех возможных комбинациях параметров проекта - компиляция кода под что-то вроде 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;

Это простое, но действенное правило.

Alex
источник
В моем сценарии I: = CreateInterfaceFromLib (...) приводил к неявному локальному. Так что то, что вы предлагаете, не поможет. В любом случае я уже ясно продемонстрировал обходной путь в вопросе. Один основан на времени жизни неявных локальных переменных, контролируемых областью действия функции. Мой вопрос касался сценариев, которые привели бы к неявным местным жителям.
Дэвид Хеффернан
Я хотел сказать, что это вообще неправильный вопрос.
Alex
1
Добро пожаловать на эту точку зрения, но вы должны выразить ее в виде комментария. Добавление кода, который пытается (безуспешно) воспроизвести обходные пути вопроса, кажется мне странным.
Дэвид Хеффернан