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


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


8
Не знаю, чому це було проти, окрім того, що питання справді складне. Проголосував за те, що мені над головою. Я знаю, що саме цей шматочок аркану призвів до незначних помилок підрахунку посилань у програмі, над якою я працював рік тому. Один з наших найкращих вундеркіндів годинами розгадував це. Врешті-решт ми обійшли це питання, але так і не зрозуміли, як задумано компілятор.
Warren P

3
@Serg Компілятор чудово зробив підрахунок посилань. Проблема полягала в тому, що в додатковій змінній міститься посилання, якого я не бачив. Я хочу знати, що спонукає компілятора взяти таку додаткову, приховану, посилання.
Девід Хеффернан,

3
Я вас розумію, але гарною практикою є написання коду, який не залежить від таких зайвих змінних. Нехай компілятор створює ці змінні скільки завгодно, суцільний код не повинен залежати від цього.
kludg

2
Ще один приклад, коли це відбувається:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle

2
У мене є спокуса назвати це помилкою компілятора ... тимчасові пристрої повинні бути очищені після того, як вони вийдуть за межі сфери дії, що має бути кінцем призначення (а не кінцем функції). Якщо цього не зробити, то виникають тонкі помилки, як ви виявили.
nneonneo

Відповіді:


15

Якщо існує якась документація про цю поведінку, можливо, вона буде знаходитися в області виробництва компілятора тимчасових змінних, щоб зберігати проміжні результати при передачі результатів функції як параметри. Розглянемо цей код:

procedure UseInterface(foo: IInterface);
begin
end;

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

Компілятор повинен створити неявну тимчасову змінну, щоб утримувати результат Create, коли він передається в UseInterface, щоб переконатися, що інтерфейс має тривалість життя = = час виклику UseInterface. Ця неявна тимчасова змінна буде видалена в кінці процедури, яка їй належить, у цьому випадку в кінці процедури Test ().

Можливо, ваш випадок призначення покажчика може потрапити в той самий сегмент, що і передача проміжних значень інтерфейсу як параметрів функції, оскільки компілятор не може "побачити", куди йде значення.

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


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;

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


У моєму сценарії я: = CreateInterfaceFromLib (...) привів до неявного локального. Тож те, що ви пропонуєте, не допоможе. У будь-якому випадку, я вже чітко продемонстрував обхідний шлях у цьому питанні. Такий, що базується на житті неявних місцевих жителів, які контролюються сферою функцій. Моє запитання стосувалося сценаріїв, які призвели б до неявних місцевих жителів.
Девід Хеффернан,

Моя думка полягала в тому, що це спочатку неправильне запитання.
Олексій

1
Вас запрошують до цієї точки зору, але ви повинні висловити це як коментар. Додавання коду, який намагається (невдало) відтворити обхідні шляхи вирішення питання, мені здається дивним.
Девід Хеффернан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.