Я просил аналогичную 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 в отдельный метод и тем самым сузил область интерфейсной переменной.
Если есть какая-либо документация по этому поведению, вероятно, будет в области создания компилятора временных переменных промежуточные результаты при передаче результатов функции в качестве параметров. Рассмотрим этот код:
procedure UseInterface(foo: IInterface);
begin
end;
procedure Test()
begin
UseInterface(Create());
end;
Компилятор должен создать неявную временную переменную, чтобы удерживать результат Create, поскольку он передается в UseInterface, чтобы убедиться, что интерфейс имеет время жизни >= время жизни вызова UseInterface. Эта неявная временная переменная будет располагаться в конце процедуры, которой она владеет, в этом случае в конце процедуры Test().
Возможно, что ваш случай назначения указателя может попасть в тот же самый ведро, что и промежуточные значения интерфейса в качестве параметров функции, поскольку компилятор не может "видеть", где это значение идет.
Я помню, что за последние годы в этой области было несколько ошибок. Давным-давно (D3? D4?) Компилятор вообще не ссылался на среднее промежуточное значение. Он работал большую часть времени, но столкнулся с проблемами в ситуациях псевдонимов параметров. После того, как это было рассмотрено, я считаю, что в отношении const params наблюдалось продолжение. Всегда было желание перенести удаление интерфейса промежуточных значений до того, как это было возможно, после заявления, в котором оно было необходимо, но я не думаю, что это когда-либо было реализовано в оптимизаторе Win32, потому что компилятор просто не был установлен для обработки утилиты при выполнении операции или блочной детализации.
Вы не можете гарантировать, что компилятор не решит создать временную невидимую переменную.
И даже если вы это сделаете, отключенная оптимизация (или даже стековые фреймы?) может испортить ваш отлично проверенный код.
И даже если вам удастся просмотреть ваш код под всеми возможными комбинациями параметров проекта - компиляция вашего кода под чем-то вроде 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;
Это простое, но эффективное правило.